Skip to content
This repository was archived by the owner on Jul 18, 2026. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 49 additions & 3 deletions packages/engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,58 @@ Includes offline queue, conflict resolution, privacy filtering, and merge strate

## CLI

The `nomark` CLI lets you import conversation exports into your NOMARK preference ledger and inspect the resulting profile.

### Install

```bash
npm install -g @nomark-ai/engine
# or use without installing:
npx nomark <command>
```

Requires Node.js 18+.

### Authenticate

Get an API key from the NOMARK panel under **Settings → API Keys**, then run:

```bash
npx nomark profile # Show resolved preference profile
npx nomark import # Import from ChatGPT/Claude exports
npx nomark review # Review and manage ledger entries
export NOMARK_TOKEN=nomark_sk_...
nomark login
```

Your key is saved to `~/.nomark/config.json` (mode 0600). To verify it's working:

```bash
nomark whoami
```

### Import

```bash
# ChatGPT — export from chat.openai.com → Settings → Export
nomark import --platform chatgpt --file conversations.json

# Claude — export from claude.ai → Settings → Export data
nomark import --platform claude --file claude-export.json

# Gemini — export from Google Takeout (My Activity → Gemini)
nomark import --platform gemini --file takeout.json
```

Each import prints a summary: conversations analyzed, signals extracted, and how many were promoted into your ledger.

### Managing API Keys

| Command | Description |
|---------|-------------|
| `nomark login` | Save an API key from `NOMARK_TOKEN` env or interactive prompt |
| `nomark whoami` | Verify the saved key and show its last four characters |
| `nomark config --model <id>` | Set the default model |

Keys are stored as `api_key` in `~/.nomark/config.json`. The file is created with mode 0600 and is never logged or transmitted anywhere except the NOMARK API.

## API Reference

### Core
Expand Down
41 changes: 31 additions & 10 deletions packages/engine/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,55 @@ export type NomarkConfig = {
model?: string
apiKey?: string
ledgerPath?: string
token?: string
}

const CONFIG_DIR = path.join(os.homedir(), '.nomark')
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json')
function getConfigDir(): string {
return path.join(os.homedir(), '.nomark')
}

function getConfigPath(): string {
return path.join(getConfigDir(), 'config.json')
}

export function loadConfig(): NomarkConfig {
const config: NomarkConfig = {}
const configPath = getConfigPath()

// File config
if (fs.existsSync(CONFIG_PATH)) {
if (fs.existsSync(configPath)) {
try {
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) as Record<string, unknown>
const raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) as Record<string, unknown>
if (typeof raw['model'] === 'string') config.model = raw['model']
if (typeof raw['apiKey'] === 'string') config.apiKey = raw['apiKey']
if (typeof raw['ledgerPath'] === 'string') config.ledgerPath = raw['ledgerPath']
if (typeof raw['api_key'] === 'string') config.token = raw['api_key']
} catch {
// ignore malformed config
}
}

// Env overrides
if (process.env['NOMARK_MODEL']) config.model = process.env['NOMARK_MODEL']
if (process.env['NOMARK_API_KEY']) config.apiKey = process.env['NOMARK_API_KEY']
if (process.env['NOMARK_TOKEN']) config.token = process.env['NOMARK_TOKEN']

return config
}

export function saveConfig(config: NomarkConfig): void {
fs.mkdirSync(CONFIG_DIR, { recursive: true })
const configDir = getConfigDir()
const configPath = getConfigPath()
fs.mkdirSync(configDir, { recursive: true })
const existing = loadConfig()
const merged = { ...existing, ...config }
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + '\n')

const json: Record<string, unknown> = {}
if (merged.model !== undefined) json['model'] = merged.model
if (merged.apiKey !== undefined) json['apiKey'] = merged.apiKey
if (merged.ledgerPath !== undefined) json['ledgerPath'] = merged.ledgerPath
if (merged.token !== undefined) json['api_key'] = merged.token

fs.writeFileSync(configPath, JSON.stringify(json, null, 2) + '\n')
fs.chmodSync(configPath, 0o600)
}

export function configCommand(flags: Record<string, string | boolean>): void {
Expand All @@ -48,9 +65,13 @@ export function configCommand(flags: Record<string, string | boolean>): void {
if (typeof flags['ledger'] === 'string') updates.ledgerPath = flags['ledger']

if (Object.keys(updates).length === 0) {
// Show current config
const config = loadConfig()
console.log(JSON.stringify(config, null, 2))
const display: Record<string, unknown> = {}
if (config.model !== undefined) display['model'] = config.model
if (config.apiKey !== undefined) display['apiKey'] = `***${config.apiKey.slice(-4)}`
if (config.ledgerPath !== undefined) display['ledgerPath'] = config.ledgerPath
if (config.token !== undefined) display['api_key'] = `***${config.token.slice(-4)}`
console.log(JSON.stringify(display, null, 2))
return
}

Expand Down
140 changes: 83 additions & 57 deletions packages/engine/src/cli/import.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,110 @@
import * as fs from 'node:fs'
import * as path from 'node:path'
import { parseChatGPTExport } from '../importers/chatgpt.js'
import { parseClaudeExport } from '../importers/claude.js'
import { runMigration } from '../importers/pipeline.js'
import { writeLedger, parseLedger } from '../ledger.js'
import type { Conversation } from '../importers/types.js'
import { loadConfig } from './config.js'
import { requireFlag } from './args.js'

const DEFAULT_LEDGER = './nomark-ledger.jsonl'
const PROCESS_IMPORT_URL = 'https://cnwiskdzeygqxezmazoq.supabase.co/functions/v1/process-import'

export function importCommand(flags: Record<string, string | boolean>): void {
const platform = requireFlag(flags, 'platform')
type Platform = 'chatgpt' | 'claude' | 'gemini'

const SUPPORTED_PLATFORMS: Platform[] = ['chatgpt', 'claude', 'gemini']

type ImportResult = {
platform: string
conversationsAnalyzed: number
signalsExtracted: number
signalsPromoted: number
byConfidence: {
high: number
medium: number
low: number
}
}

export async function importCommand(flags: Record<string, string | boolean>): Promise<void> {
const platform = requireFlag(flags, 'platform') as Platform
const filePath = requireFlag(flags, 'file')
const dryRun = flags['dry-run'] !== false && flags['dry-run'] !== 'false'
const ledgerPath = typeof flags['ledger'] === 'string' ? flags['ledger'] : DEFAULT_LEDGER
const maxConversations = typeof flags['max'] === 'string' ? parseInt(flags['max'], 10) : undefined

// Read file
if (!SUPPORTED_PLATFORMS.includes(platform)) {
console.error(`Unsupported platform: ${platform}. Supported: ${SUPPORTED_PLATFORMS.join(', ')}`)
process.exit(1)
}

if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`)
process.exit(1)
}

const raw = fs.readFileSync(filePath, 'utf8')

const parsers: Record<string, (data: string) => Conversation[]> = {
chatgpt: parseChatGPTExport,
claude: parseClaudeExport,
let raw: string
try {
raw = fs.readFileSync(filePath, 'utf8')
} catch {
console.error(`Error: Could not read file: ${filePath}`)
process.exit(1)
return
}

const parser = parsers[platform]
if (!parser) {
console.error(`Unsupported platform: ${platform}. Supported: chatgpt, claude`)
let data: unknown
try {
data = JSON.parse(raw)
} catch {
console.error(`Error: Invalid JSON in file: ${filePath}`)
process.exit(1)
return // unreachable but satisfies TS
return
}

const conversations = parser(raw)
const config = loadConfig()
const token = config.token

if (conversations.length === 0) {
console.log('No conversations found in export file.')
return
if (!token) {
console.error('Not authenticated. Run `nomark login` first.')
process.exit(1)
}

// Load existing ledger for dedup
let existingLedger = ''
if (fs.existsSync(ledgerPath)) {
existingLedger = fs.readFileSync(ledgerPath, 'utf8')
let response: Response
try {
response = await fetch(PROCESS_IMPORT_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ platform, data }),
})
} catch {
console.error('Error: Could not reach NOMARK API. Check your network connection.')
process.exit(1)
return
}

// Run pipeline
const report = runMigration(conversations, {
existingLedger: existingLedger || undefined,
dryRun,
maxConversations,
})

// Display report
console.log()
console.log(` Analyzing ${report.conversationsAnalyzed} conversations...`)
console.log(` Extracted ${report.signalsExtracted} signals (${report.byConfidence.high} high, ${report.byConfidence.medium} medium, ${report.byConfidence.low} low confidence)`)
console.log(` Promoted ${report.signalsPromoted} to ledger.`)

if (report.byConfidence.low > 0) {
console.log(` ${report.byConfidence.low} need review. Run: npx nomark review`)
if (!response.ok) {
let body = ''
try {
body = await response.text()
} catch {
// ignore read failure
}
if (response.status === 401) {
console.error('Error: Authentication failed (401). Run `nomark login` to re-authenticate.')
} else {
console.error(`Error: API request failed (${response.status}): ${body}`)
}
process.exit(1)
return
}

if (dryRun) {
console.log()
console.log(' [DRY RUN] No changes written. Run with --dry-run=false to apply.')
} else {
// Write ledger
const existingEntries = existingLedger ? parseLedger(existingLedger) : []
const allEntries = [...existingEntries, ...report.ledgerEntries]
const content = writeLedger(allEntries)
fs.mkdirSync(path.dirname(path.resolve(ledgerPath)), { recursive: true })
fs.writeFileSync(ledgerPath, content)
console.log(` Written to ${ledgerPath}`)
let result: ImportResult
try {
result = (await response.json()) as ImportResult
} catch {
console.error('Error: Invalid response from NOMARK API.')
process.exit(1)
return
}

console.log(' No account needed. Everything runs locally.')
console.log()
console.log(`Import complete. Platform: ${result.platform}.`)
console.log(`Conversations analyzed: ${result.conversationsAnalyzed}.`)
console.log(`Signals extracted: ${result.signalsExtracted} (${result.byConfidence.high} high, ${result.byConfidence.medium} medium, ${result.byConfidence.low} low confidence).`)
console.log(`Signals promoted to ledger: ${result.signalsPromoted}.`)
console.log()
}
24 changes: 17 additions & 7 deletions packages/engine/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import { parseArgs } from './args.js'
import { configCommand } from './config.js'
import { importCommand } from './import.js'
import { loginCommand } from './login.js'
import { profileCommand } from './profile.js'
import { reviewCommand } from './review.js'
import { whoamiCommand } from './whoami.js'

const VERSION = '0.1.0'
const VERSION = '0.2.1'

const HELP = `
nomark — agent outcome quality engine
Expand All @@ -15,6 +17,8 @@ const HELP = `
npx nomark <command> [options]

Commands:
login Authenticate with your NOMARK API key
whoami Show current authentication status
config Configure model and API key
import Import AI conversation history
profile View your preference profile
Expand All @@ -25,13 +29,13 @@ const HELP = `
--version Show version

Examples:
npx nomark config --model claude --api-key $ANTHROPIC_API_KEY
npx nomark import --platform chatgpt --file export.json
npx nomark profile
npx nomark review
npx nomark login
npx nomark import --platform chatgpt --file conversations.json
npx nomark import --platform claude --file claude-export.json
npx nomark import --platform gemini --file takeout.json
`

function main(): void {
async function main(): Promise<void> {
const { command, flags } = parseArgs(process.argv)

if (flags['version']) {
Expand All @@ -46,11 +50,17 @@ function main(): void {

try {
switch (command) {
case 'login':
await loginCommand()
break
case 'whoami':
await whoamiCommand()
break
case 'config':
configCommand(flags)
break
case 'import':
importCommand(flags)
await importCommand(flags)
break
case 'profile':
profileCommand(flags)
Expand Down
Loading
Loading