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
8 changes: 4 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,20 @@ jobs:

- run: npm run build --workspaces

- run: npm test --workspaces
- run: npm test --workspace packages/engine

- working-directory: packages/engine
run: npm publish --access public || true
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- working-directory: packages/pro
run: npm publish --access public || true
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- working-directory: packages/create-nomark-engine
run: npm publish --access public || true
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Expand Down
68 changes: 66 additions & 2 deletions packages/engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ The open-core preference resolution engine for AI agents. Learns what humans mea
## Install

```bash
# As a library
npm install @nomark-ai/engine

# As a CLI (no install required)
npx nomark --help
```

Requires Node.js 18+.
Expand Down Expand Up @@ -110,9 +114,69 @@ Includes offline queue, conflict resolution, privacy filtering, and merge strate
## CLI

```bash
npx nomark login # Save your API key
npx nomark whoami # Check authentication status
npx nomark import --platform chatgpt --file conversations.json
npx nomark profile # Show resolved preference profile
npx nomark import # Import from ChatGPT/Claude exports
npx nomark review # Review and manage ledger entries
npx nomark review # Review low-confidence signals
```

### Authenticate

Get an API key from [nomark.ai](https://nomark.ai) and run:

```bash
npx nomark login
```

You will be prompted to enter your key (`nomark_sk_...`). Alternatively, set the `NOMARK_TOKEN` environment variable to skip interactive login:

```bash
export NOMARK_TOKEN=nomark_sk_...
npx nomark import --platform chatgpt --file conversations.json
```

The key is stored in `~/.nomark/config.json` with permissions `0600`.

### Import

Import your conversation history to build your NOMARK preference profile:

```bash
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
```

**Supported platforms:**

| Platform | Export file | How to export |
|----------|-------------|---------------|
| `chatgpt` | `conversations.json` | Settings → Data controls → Export data |
| `claude` | `claude-export.json` | Settings → Privacy → Export data |
| `gemini` | `Takeout/...json` | Google Takeout → Gemini Apps activity |

After a successful import you will see:

```
Analyzed 142 conversations from chatgpt.
Extracted 38 signals (12 high, 18 medium, 8 low confidence).
Promoted 30 to your NOMARK profile.
```

### Managing API Keys

| Task | Command |
|------|---------|
| Save a new key | `npx nomark login` |
| Check current auth | `npx nomark whoami` |
| Update key | Run `npx nomark login` again — it overwrites the existing key |
| Remove key | Delete `~/.nomark/config.json` |

To use a different key for a single command without modifying the stored config:

```bash
NOMARK_TOKEN=nomark_sk_... npx nomark import --platform chatgpt --file conversations.json
```

## API Reference
Expand Down
23 changes: 15 additions & 8 deletions packages/engine/src/cli/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as os from 'node:os'

export type NomarkConfig = {
model?: string
apiKey?: string
api_key?: string
ledgerPath?: string
}

Expand All @@ -14,12 +14,13 @@ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json')
export function loadConfig(): NomarkConfig {
const config: NomarkConfig = {}

// File config
if (fs.existsSync(CONFIG_PATH)) {
try {
const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8')) as Record<string, unknown>
if (typeof raw['model'] === 'string') config.model = raw['model']
if (typeof raw['apiKey'] === 'string') config.apiKey = raw['apiKey']
// Support both snake_case (current) and legacy camelCase
if (typeof raw['api_key'] === 'string') config.api_key = raw['api_key']
else if (typeof raw['apiKey'] === 'string') config.api_key = raw['apiKey']
if (typeof raw['ledgerPath'] === 'string') config.ledgerPath = raw['ledgerPath']
} catch {
// ignore malformed config
Expand All @@ -28,7 +29,7 @@ export function loadConfig(): NomarkConfig {

// 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.api_key = process.env['NOMARK_TOKEN']

return config
}
Expand All @@ -38,25 +39,31 @@ export function saveConfig(config: NomarkConfig): void {
const existing = loadConfig()
const merged = { ...existing, ...config }
fs.writeFileSync(CONFIG_PATH, JSON.stringify(merged, null, 2) + '\n')
fs.chmodSync(CONFIG_PATH, 0o600)
}

export function loadApiKey(): string | undefined {
return process.env['NOMARK_TOKEN'] ?? loadConfig().api_key
}

export function configCommand(flags: Record<string, string | boolean>): void {
const updates: NomarkConfig = {}

if (typeof flags['model'] === 'string') updates.model = flags['model']
if (typeof flags['api-key'] === 'string') updates.apiKey = flags['api-key']
if (typeof flags['api-key'] === 'string') updates.api_key = flags['api-key']
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 = { ...config }
if (display.api_key) display.api_key = `***${display.api_key.slice(-4)}`
console.log(JSON.stringify(display, null, 2))
return
}

saveConfig(updates)
console.log('Configuration saved to ~/.nomark/config.json')
if (updates.model) console.log(` model: ${updates.model}`)
if (updates.apiKey) console.log(` apiKey: ***${updates.apiKey.slice(-4)}`)
if (updates.api_key) console.log(` api_key: ***${updates.api_key.slice(-4)}`)
if (updates.ledgerPath) console.log(` ledgerPath: ${updates.ledgerPath}`)
}
118 changes: 59 additions & 59 deletions packages/engine/src/cli/import.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,84 @@
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 { loadApiKey } 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 SUPPORTED_PLATFORMS = ['chatgpt', 'claude', 'gemini'] as const

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')
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 (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`)
if (!(SUPPORTED_PLATFORMS as readonly string[]).includes(platform)) {
console.error(`Unsupported platform: ${platform}. Supported: ${SUPPORTED_PLATFORMS.join(', ')}`)
process.exit(1)
}

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

const parsers: Record<string, (data: string) => Conversation[]> = {
chatgpt: parseChatGPTExport,
claude: parseClaudeExport,
const apiKey = loadApiKey()
if (!apiKey) {
console.error('Not authenticated. Run `nomark login` first.')
process.exit(1)
}

const parser = parsers[platform]
if (!parser) {
console.error(`Unsupported platform: ${platform}. Supported: chatgpt, claude`)
if (!fs.existsSync(filePath)) {
console.error(`File not found: ${filePath}`)
process.exit(1)
return // unreachable but satisfies TS
}

const conversations = parser(raw)

if (conversations.length === 0) {
console.log('No conversations found in export file.')
return
let data: unknown
try {
const raw = fs.readFileSync(filePath, 'utf8')
data = JSON.parse(raw)
} catch {
console.error(`Failed to parse file as JSON: ${filePath}`)
process.exit(1)
}

// Load existing ledger for dedup
let existingLedger = ''
if (fs.existsSync(ledgerPath)) {
existingLedger = fs.readFileSync(ledgerPath, 'utf8')
let res: Response
try {
res = await fetch(PROCESS_IMPORT_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ platform, data }),
})
} catch {
console.error('Network error. Check your connection and try again.')
process.exit(1)
}

// 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 (!res.ok) {
if (res.status === 401 || res.status === 403) {
console.error('Not authenticated. Run `nomark login` to refresh your API key.')
process.exit(1)
}
console.error(`Import failed (HTTP ${res.status}). Try again or contact support.`)
process.exit(1)
}

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 res.json()) as ImportResult
} catch {
console.error('Unexpected response from server.')
process.exit(1)
}

console.log(' No account needed. Everything runs locally.')
console.log()
console.log(` Analyzed ${result.conversationsAnalyzed} conversations from ${platform}.`)
console.log(` Extracted ${result.signalsExtracted} signals (${result.byConfidence.high} high, ${result.byConfidence.medium} medium, ${result.byConfidence.low} low confidence).`)
console.log(` Promoted ${result.signalsPromoted} to your NOMARK profile.`)
console.log()
}
27 changes: 22 additions & 5 deletions packages/engine/src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
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'

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

Commands:
login Save your NOMARK API key
whoami Check authentication status
config Configure model and API key
import Import AI conversation history
profile View your preference profile
Expand All @@ -25,13 +29,15 @@ 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 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
npx nomark profile
npx nomark review
`

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

if (flags['version']) {
Expand All @@ -46,11 +52,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 All @@ -71,4 +83,9 @@ function main(): void {
}
}

main()
main().catch((err: unknown) => {
if (err instanceof Error) {
console.error(`Error: ${err.message}`)
}
process.exit(1)
})
Loading
Loading