scalix 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,149 @@
1
+ // OpenAI-compatible endpoint adapter (BYOK).
2
+ //
3
+ // Speaks the standard OpenAI wire — the same request/response dialect the
4
+ // agent already produces/consumes — against any server that hosts it:
5
+ // OpenAI, OpenRouter, Ollama, LM Studio, Together, vLLM, LiteLLM, ...
6
+ // The only differences from the Scalix client are URL composition
7
+ // ({root}/chat/completions instead of the gateway's /v1/ai nest) and
8
+ // dropping the Scalix-only `thinking_level` field, which strict servers
9
+ // (OpenAI itself) reject as an unrecognized argument.
10
+ import { ScalixClient } from '../api-client.js';
11
+ import { WEB_DEFAULTS } from '../constants.js';
12
+ /**
13
+ * Normalize a user-supplied OpenAI-compatible base URL to the endpoint root
14
+ * that `/chat/completions` and `/models` append to. Accepts both conventions:
15
+ * https://api.openai.com/v1 → https://api.openai.com/v1
16
+ * https://openrouter.ai/api/v1 → https://openrouter.ai/api/v1
17
+ * http://localhost:11434 → http://localhost:11434/v1
18
+ * http://localhost:1234/ → http://localhost:1234/v1
19
+ * A versioned path segment anywhere (v1, v2, ...) is treated as already-rooted.
20
+ */
21
+ export function openaiEndpointRoot(baseUrl) {
22
+ const trimmed = baseUrl.replace(/\/+$/, '');
23
+ try {
24
+ const u = new URL(trimmed);
25
+ if (/\/v\d+($|\/)/.test(u.pathname))
26
+ return trimmed;
27
+ }
28
+ catch {
29
+ // Not parseable as a URL — leave composition to fetch to surface the error.
30
+ return trimmed;
31
+ }
32
+ return `${trimmed}/v1`;
33
+ }
34
+ export class OpenAICompatClient extends ScalixClient {
35
+ chatUrl() {
36
+ return `${openaiEndpointRoot(this.baseURL)}/chat/completions`;
37
+ }
38
+ modelsUrl() {
39
+ return openaiModelsUrl(this.baseURL);
40
+ }
41
+ /**
42
+ * The Scalix output ceiling is a property of the Scalix platform, so it must
43
+ * not be imposed on — or inferred from — someone else's endpoint. The agent
44
+ * already sends BYOK a max_tokens derived from the user's own model spec (or
45
+ * none at all), and a third-party server that rejects that value is telling
46
+ * the user their configuration is wrong: surface it instead of quietly
47
+ * retrying at a Scalix-shaped number that means nothing here.
48
+ */
49
+ canLowerMaxTokens() {
50
+ return false;
51
+ }
52
+ // Deliberately does NOT delegate to super: the base implementation clamps
53
+ // max_tokens to the Scalix ceiling, which is not this endpoint's business.
54
+ prepareRequest(req) {
55
+ let out = req;
56
+ // `thinking_level` is a Scalix extension; strict OpenAI-compatible servers
57
+ // 400 on unrecognized arguments. Reasoning output isn't consumed by the
58
+ // agent today, so dropping it is lossless (spec §3.8).
59
+ if (out.thinking_level !== undefined) {
60
+ const { thinking_level: _drop, ...rest } = out;
61
+ out = rest;
62
+ }
63
+ // An OpenAI-compatible server sends a usage chunk on a stream only when it
64
+ // is asked for. Without this a whole BYOK session streams with no token
65
+ // figures at all: the status bar, /cost and the context meter all stay on
66
+ // zero, and the context meter is what tells the user compression is near.
67
+ // (The Scalix gateway emits a final usage chunk unconditionally, so its
68
+ // own client has no need of the field.) If the endpoint turns out not to
69
+ // know `stream_options`, the base client drops it after the first 400 and
70
+ // sets this flag.
71
+ if (out.stream && out.stream_options === undefined && !this.streamUsageUnsupported) {
72
+ out = { ...out, stream_options: { include_usage: true } };
73
+ }
74
+ return out;
75
+ }
76
+ }
77
+ /** The /models URL for an OpenAI-compatible endpoint (OpenAI list shape). */
78
+ export function openaiModelsUrl(baseUrl) {
79
+ return `${openaiEndpointRoot(baseUrl)}/models`;
80
+ }
81
+ // Everyone agrees on `id` and nobody agrees on the context window: OpenRouter
82
+ // sends context_length, vLLM max_model_len, LM Studio max_context_length,
83
+ // Scalix context_window, and OpenAI itself sends none at all. Read whichever
84
+ // is there; a model that reports nothing keeps contextWindow undefined rather
85
+ // than inheriting someone else's number.
86
+ const CONTEXT_WINDOW_KEYS = ['context_length', 'max_model_len', 'max_context_length', 'context_window'];
87
+ function reportedContextWindow(model) {
88
+ const top = model.top_provider;
89
+ for (const source of [model, top]) {
90
+ if (!source)
91
+ continue;
92
+ for (const key of CONTEXT_WINDOW_KEYS) {
93
+ const v = source[key];
94
+ if (typeof v === 'number' && Number.isFinite(v) && v > 0)
95
+ return Math.floor(v);
96
+ }
97
+ }
98
+ return undefined;
99
+ }
100
+ /**
101
+ * List the models a BYOK endpoint actually serves.
102
+ *
103
+ * Without this the setup wizard can only tell the user to go read
104
+ * `GET /v1/models` themselves and type the ids back in, and one typo produces a
105
+ * 404 on the first turn with nothing to suggest the id was the problem.
106
+ *
107
+ * Deliberately does not touch the model-spec registry: on the Scalix path a
108
+ * reported spec is the platform's own number, but a third-party endpoint that
109
+ * reports a context window and no output ceiling would have one invented for
110
+ * it here, and inventing a ceiling for someone else's model is how you get
111
+ * rejected requests. The window is returned for the wizard to show and is
112
+ * never combined with a default.
113
+ *
114
+ * Returns [] for anything that isn't a usable answer — this only ever feeds a
115
+ * picker that the user can bypass by typing an id.
116
+ */
117
+ export async function fetchByokModels(baseUrl, apiKey) {
118
+ if (!baseUrl)
119
+ return [];
120
+ try {
121
+ const headers = {};
122
+ // Ollama and LM Studio serve /models unauthenticated; sending an empty
123
+ // bearer to an endpoint that does check is a 401 for no reason.
124
+ if (apiKey)
125
+ headers['Authorization'] = `Bearer ${apiKey}`;
126
+ const res = await fetch(openaiModelsUrl(baseUrl), {
127
+ headers,
128
+ signal: AbortSignal.timeout(WEB_DEFAULTS.apiFetchTimeoutMs),
129
+ });
130
+ if (!res.ok)
131
+ return [];
132
+ const body = await res.json();
133
+ if (!Array.isArray(body.data))
134
+ return [];
135
+ const out = [];
136
+ const seen = new Set();
137
+ for (const entry of body.data) {
138
+ const m = entry;
139
+ if (typeof m?.id !== 'string' || !m.id || seen.has(m.id))
140
+ continue;
141
+ seen.add(m.id);
142
+ out.push({ id: m.id, contextWindow: reportedContextWindow(m) });
143
+ }
144
+ return out;
145
+ }
146
+ catch {
147
+ return [];
148
+ }
149
+ }
package/dist/settings.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { setSecurityConfig, setScopedPermissionRules } from './sandbox.js';
4
- import { DEFAULT_BASE_URL, DEFAULT_MODELS, AGENT_DEFAULTS, CONTEXT_DEFAULTS, WEB_DEFAULTS, TOOL_DEFAULTS, SUB_AGENT_DEFAULTS, MULTI_AGENT_DEFAULTS, USER_CONFIG_DIR, registerUserModels, setNetworkSecurityOverrides, } from './constants.js';
4
+ import { DEFAULT_BASE_URL, DEFAULT_MODELS, AGENT_DEFAULTS, CONTEXT_DEFAULTS, WEB_DEFAULTS, TOOL_DEFAULTS, SUB_AGENT_DEFAULTS, MULTI_AGENT_DEFAULTS, USER_CONFIG_DIR, registerUserModels, setNetworkSecurityOverrides, normalizeModelId, } from './constants.js';
5
5
  import { getApiKeyFromKeychain, setApiKeyInKeychain, isKeychainAvailable } from './keychain.js';
6
+ import { isProviderId } from './providers/index.js';
6
7
  import { isWorkspaceTrusted } from './workspace-trust.js';
7
8
  function getConfigDir() {
8
9
  return path.join(process.env.HOME || process.env.USERPROFILE || '.', USER_CONFIG_DIR);
@@ -77,7 +78,7 @@ function validateConfig(parsed) {
77
78
  if (typeof parsed.baseUrl === 'string' && parsed.baseUrl.length > 0)
78
79
  safe.baseUrl = parsed.baseUrl;
79
80
  if (Array.isArray(parsed.models) && parsed.models.every((m) => typeof m === 'string')) {
80
- safe.models = parsed.models;
81
+ safe.models = parsed.models.map(normalizeModelId);
81
82
  }
82
83
  if (typeof parsed.permissionMode === 'string' && ['plan', 'default', 'auto', 'yolo'].includes(parsed.permissionMode)) {
83
84
  safe.permissionMode = parsed.permissionMode;
@@ -160,6 +161,23 @@ function validateConfig(parsed) {
160
161
  if (typeof parsed.disableUpdateCheck === 'boolean') {
161
162
  safe.disableUpdateCheck = parsed.disableUpdateCheck;
162
163
  }
164
+ // BYOK (validateConfig drops unknown keys, so these MUST be whitelisted here
165
+ // or they vanish on the next saveConfig — spec §4 migration gotcha #1).
166
+ if (isProviderId(parsed.provider)) {
167
+ safe.provider = parsed.provider;
168
+ }
169
+ if (parsed.providers && typeof parsed.providers === 'object' && !Array.isArray(parsed.providers)) {
170
+ const po = parsed.providers.openai;
171
+ if (po && typeof po === 'object' && !Array.isArray(po)) {
172
+ const creds = {};
173
+ if (typeof po.apiKey === 'string')
174
+ creds.apiKey = po.apiKey;
175
+ if (typeof po.baseUrl === 'string' && po.baseUrl.length > 0)
176
+ creds.baseUrl = po.baseUrl;
177
+ if (Object.keys(creds).length > 0)
178
+ safe.providers = { openai: creds };
179
+ }
180
+ }
163
181
  return safe;
164
182
  }
165
183
  function isStringArray(val) {
@@ -339,6 +357,12 @@ export async function loadConfig() {
339
357
  if (keychainKey)
340
358
  config.apiKey = keychainKey;
341
359
  }
360
+ // Per-provider keychain slot (BYOK): openai key lives under its own account.
361
+ if (config.providers?.openai && !config.providers.openai.apiKey) {
362
+ const k = await getApiKeyFromKeychain(OPENAI_KEYCHAIN_ACCOUNT).catch(() => null);
363
+ if (k)
364
+ config.providers.openai.apiKey = k;
365
+ }
342
366
  // Apply local overrides (project-level, gitignored)
343
367
  config = await loadLocalOverrides(config);
344
368
  // Apply managed/organization settings (enterprise policy)
@@ -365,16 +389,70 @@ export async function loadConfig() {
365
389
  }
366
390
  export async function saveConfig(config) {
367
391
  await ensureDir();
368
- // Store API key in keychain if available, strip from config file
392
+ // Store API keys in keychain if available, strip from config file. The
393
+ // per-provider secret gets the same blanking treatment as the top-level key
394
+ // (spec §4: new creds must never persist plaintext when a keychain exists).
369
395
  const configToWrite = { ...config };
370
- if (config.apiKey && await isKeychainAvailable()) {
371
- const stored = await setApiKeyInKeychain(config.apiKey).catch(() => false);
372
- if (stored) {
373
- configToWrite.apiKey = '';
396
+ let plaintextSecret = false;
397
+ if (await isKeychainAvailable()) {
398
+ if (config.apiKey) {
399
+ const stored = await setApiKeyInKeychain(config.apiKey).catch(() => false);
400
+ if (stored)
401
+ configToWrite.apiKey = '';
402
+ else
403
+ plaintextSecret = true;
404
+ }
405
+ if (config.providers?.openai?.apiKey) {
406
+ const stored = await setApiKeyInKeychain(config.providers.openai.apiKey, OPENAI_KEYCHAIN_ACCOUNT).catch(() => false);
407
+ if (stored) {
408
+ configToWrite.providers = {
409
+ ...configToWrite.providers,
410
+ openai: { ...configToWrite.providers.openai, apiKey: '' },
411
+ };
412
+ }
413
+ else {
414
+ plaintextSecret = true;
415
+ }
374
416
  }
375
417
  }
376
418
  await fs.writeFile(getConfigFile(), JSON.stringify(configToWrite, null, 2), { encoding: 'utf-8', mode: 0o600 });
419
+ return { plaintextSecret };
377
420
  }
378
421
  export function isConfigured(config) {
379
- return config.apiKey.length > 0 && config.models.length > 0;
422
+ const active = resolveActiveProvider(config);
423
+ if (active.provider === 'scalix') {
424
+ return config.apiKey.length > 0 && config.models.length > 0;
425
+ }
426
+ return active.apiKey.length > 0 && active.baseUrl.length > 0 && config.models.length > 0;
427
+ }
428
+ // ─── BYOK provider resolution ───
429
+ export const OPENAI_KEYCHAIN_ACCOUNT = 'api-key-openai';
430
+ /**
431
+ * Resolve which provider + credentials this session runs on.
432
+ *
433
+ * Precedence (mirrors the rest of config): env → config. `SCALIX_PROVIDER`
434
+ * selects the provider; for `openai`, `OPENAI_API_KEY` / `OPENAI_BASE_URL`
435
+ * override `providers.openai`. Scalix stays the zero-config default and its
436
+ * path is byte-identical to the pre-BYOK behavior. There is NO default
437
+ * third-party URL — an openai selection without a baseUrl resolves with an
438
+ * empty one and the caller surfaces the configuration error.
439
+ */
440
+ export function resolveActiveProvider(config, env = process.env) {
441
+ const envProvider = env.SCALIX_PROVIDER?.trim().toLowerCase();
442
+ const provider = isProviderId(envProvider)
443
+ ? envProvider
444
+ : (config.provider ?? 'scalix');
445
+ if (provider === 'openai') {
446
+ const creds = config.providers?.openai ?? {};
447
+ return {
448
+ provider,
449
+ baseUrl: env.OPENAI_BASE_URL?.trim() || creds.baseUrl || '',
450
+ apiKey: env.OPENAI_API_KEY?.trim() || creds.apiKey || '',
451
+ };
452
+ }
453
+ return {
454
+ provider: 'scalix',
455
+ baseUrl: env.SCALIX_BASE_URL?.trim() || config.baseUrl,
456
+ apiKey: env.SCALIX_API_KEY?.trim() || config.apiKey,
457
+ };
380
458
  }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The slash-command catalog, in the two shapes the TUI needs it.
3
+ *
4
+ * These used to be two array literals a thousand lines apart inside cli.tsx and
5
+ * they had drifted: /update was reachable but absent from the typeahead, while
6
+ * /fork, /permissions, /resume and /skills were in the typeahead but missing
7
+ * from /help. Keeping both here lets slash-commands.test.ts assert they stay in
8
+ * agreement, so a command added to one is never silently missing from the other.
9
+ */
10
+ /** Every built-in command, as offered by the "/" typeahead. */
11
+ export const SLASH_COMMANDS = [
12
+ { cmd: '/help', desc: 'Show all commands' },
13
+ { cmd: '/clear', desc: 'Reset conversation' },
14
+ { cmd: '/models', desc: 'List active models' },
15
+ { cmd: '/model', desc: 'Switch model' },
16
+ { cmd: '/mode', desc: 'Permission mode (plan/default/auto/yolo)' },
17
+ { cmd: '/thinking', desc: 'Thinking mode (off/low/medium/high)' },
18
+ { cmd: '/plan', desc: 'Enter plan mode (read-only)' },
19
+ { cmd: '/compact', desc: 'Compress conversation context' },
20
+ { cmd: '/context', desc: 'Show context usage & token count' },
21
+ { cmd: '/cost', desc: 'Show token usage & cost' },
22
+ { cmd: '/diff', desc: 'Show uncommitted changes' },
23
+ { cmd: '/copy', desc: 'Copy last response to clipboard' },
24
+ { cmd: '/save', desc: 'Save current session' },
25
+ { cmd: '/load', desc: 'Resume a saved session' },
26
+ { cmd: '/resume', desc: 'Resume from crash recovery or saved session' },
27
+ { cmd: '/fork', desc: 'Fork conversation from a point in history' },
28
+ { cmd: '/export', desc: 'Export conversation as text' },
29
+ { cmd: '/index', desc: 'Scan & index project files' },
30
+ { cmd: '/init', desc: 'Initialize project config (SCALIX.md)' },
31
+ { cmd: '/git', desc: 'Show git context' },
32
+ { cmd: '/memory', desc: 'Show project memory' },
33
+ { cmd: '/remember', desc: 'Save a note about this project' },
34
+ { cmd: '/replay', desc: 'Show session timeline' },
35
+ { cmd: '/plugins', desc: 'Show loaded plugins' },
36
+ { cmd: '/trust', desc: 'Trust this workspace (load project plugins/hooks/rules)' },
37
+ { cmd: '/router', desc: 'Show model routing status' },
38
+ { cmd: '/status', desc: 'Version, model & connection info' },
39
+ { cmd: '/update', desc: 'Check for and install updates' },
40
+ { cmd: '/doctor', desc: 'Diagnose installation' },
41
+ { cmd: '/mcp', desc: 'Add MCP server' },
42
+ { cmd: '/cloud', desc: 'Scalix Cloud status, platform tools & refresh' },
43
+ { cmd: '/login', desc: 'Link your Scalix account (API key)' },
44
+ { cmd: '/setup', desc: 'Reconfigure API key, URL & models' },
45
+ { cmd: '/settings', desc: 'View & configure limits' },
46
+ { cmd: '/permissions', desc: 'View permission rules & recent denials' },
47
+ { cmd: '/shortcuts', desc: 'Keyboard shortcuts' },
48
+ { cmd: '/hooks', desc: 'Show lifecycle hooks' },
49
+ { cmd: '/commands', desc: 'Show custom commands' },
50
+ { cmd: '/skills', desc: 'List available skills' },
51
+ { cmd: '/delegate', desc: 'Run task with sub-agent' },
52
+ { cmd: '/agents', desc: 'Show active agents & usage' },
53
+ { cmd: '/checkpoint', desc: 'Create git checkpoint' },
54
+ { cmd: '/undo', desc: 'Undo last checkpoint' },
55
+ { cmd: '/checkpoints', desc: 'List recent checkpoints' },
56
+ { cmd: '/commit', desc: 'Auto-generate commit message & commit' },
57
+ { cmd: '/review', desc: 'AI code review of current changes' },
58
+ { cmd: '/watch', desc: 'Run command, auto-fix on failure' },
59
+ { cmd: '/create', desc: 'Scaffold a new project' },
60
+ { cmd: '/worktree', desc: 'Manage git worktrees for isolated work' },
61
+ { cmd: '/plugin', desc: 'Install/enable/disable/uninstall plugins' },
62
+ { cmd: '/notify', desc: 'Test terminal notification' },
63
+ { cmd: '/exit', desc: 'Save & exit' },
64
+ ];
65
+ /** Grouped, arg-annotated rendering of the same catalog, as printed by /help. */
66
+ export const HELP_LINES = [
67
+ 'Session & Navigation:',
68
+ ' /help \u2014 show commands',
69
+ ' /shortcuts \u2014 keyboard shortcuts',
70
+ ' /clear \u2014 reset conversation',
71
+ ' /save [name] \u2014 save current session',
72
+ ' /load \u2014 resume a saved session',
73
+ ' /resume \u2014 resume from crash recovery or saved session',
74
+ ' /fork \u2014 fork conversation from a point in history',
75
+ ' /export [f] \u2014 export conversation as text',
76
+ ' /exit \u2014 save & exit',
77
+ '',
78
+ 'Model & Mode:',
79
+ ' /models \u2014 list active models',
80
+ ' /model \u2014 switch model',
81
+ ' /mode [m] \u2014 permission mode (plan/default/auto/yolo)',
82
+ ' /thinking [m] \u2014 thinking mode (off/low/medium/high)',
83
+ ' /plan \u2014 enter plan mode (read-only)',
84
+ '',
85
+ 'Context & Cost:',
86
+ ' /compact \u2014 compress conversation context',
87
+ ' /context \u2014 show context usage & token count',
88
+ ' /cost \u2014 show token usage & cost',
89
+ '',
90
+ 'Code & Git:',
91
+ ' /diff \u2014 show uncommitted changes',
92
+ ' /commit \u2014 auto-generate commit message & commit',
93
+ ' /review \u2014 AI code review of current changes',
94
+ ' /checkpoint \u2014 create git checkpoint',
95
+ ' /undo \u2014 undo last checkpoint',
96
+ ' /checkpoints \u2014 list recent checkpoints',
97
+ ' /worktree \u2014 manage git worktrees (list/create/remove/cleanup)',
98
+ ' /git \u2014 show git context',
99
+ '',
100
+ 'Project:',
101
+ ' /init \u2014 initialize project config (SCALIX.md)',
102
+ ' /index \u2014 scan & index project files',
103
+ ' /memory \u2014 show project memory',
104
+ ' /remember <n> \u2014 save a note about this project',
105
+ ' /watch <cmd> \u2014 run command, auto-fix on failure',
106
+ ' /create <d> \u2014 scaffold a new project',
107
+ ' /delegate <t> \u2014 run task with sub-agent',
108
+ ' /agents \u2014 show active agents & usage',
109
+ '',
110
+ 'Tools & Config:',
111
+ ' /copy \u2014 copy last response to clipboard',
112
+ ' /cloud \u2014 Scalix Cloud status (/cloud tools, /cloud refresh)',
113
+ ' /login \u2014 link your Scalix account (API key)',
114
+ ' /status \u2014 show version, model & connection info',
115
+ ' /update \u2014 check for and install updates',
116
+ ' /doctor \u2014 diagnose installation',
117
+ ' /settings \u2014 view & configure limits',
118
+ ' /permissions \u2014 view permission rules & recent denials',
119
+ ' /skills \u2014 list available skills',
120
+ ' /setup \u2014 reconfigure API key, URL & models',
121
+ ' /mcp <l> <u> \u2014 add MCP server',
122
+ ' /plugins \u2014 show loaded plugins',
123
+ ' /plugin \u2014 install/enable/disable/uninstall plugins',
124
+ ' /trust \u2014 trust workspace (load project plugins/hooks/rules)',
125
+ ' /notify \u2014 test terminal notification',
126
+ ' /router \u2014 show model routing status',
127
+ ' /replay \u2014 show session timeline',
128
+ ' /hooks \u2014 show lifecycle hooks',
129
+ ' /commands \u2014 show custom commands',
130
+ '',
131
+ 'Input modes:',
132
+ ' !<cmd> \u2014 run shell command (e.g. !ls -la)',
133
+ ' text\\ \u2014 multiline input (end with \\ to continue)',
134
+ ];
135
+ /**
136
+ * Pull the command tokens out of the /help text — the leading "/word" of any
137
+ * indented entry line. Section headers and the "Input modes" rows (!<cmd>,
138
+ * text\\) have no leading slash and are skipped.
139
+ */
140
+ export function helpCommands(lines = HELP_LINES) {
141
+ const found = [];
142
+ for (const line of lines) {
143
+ const m = /^ {2}(\/[a-z-]+)/.exec(line);
144
+ if (m)
145
+ found.push(m[1]);
146
+ }
147
+ return found;
148
+ }
package/dist/sub-agent.js CHANGED
@@ -67,6 +67,7 @@ export async function spawnSubAgent(parentConfig, prompt, options = {}) {
67
67
  const subAgent = createAgent({
68
68
  apiKey: parentConfig.apiKey,
69
69
  baseUrl: parentConfig.baseUrl,
70
+ provider: parentConfig.provider,
70
71
  model,
71
72
  instructions: options.instructions || 'You are a focused research assistant. Be concise and factual. Respond in under 500 words.',
72
73
  tools,
package/package.json CHANGED
@@ -1,11 +1,17 @@
1
1
  {
2
2
  "name": "scalix",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "AI coding agent for the terminal — edit code, run commands, and build with Scalix",
5
5
  "main": "dist/headless.js",
6
6
  "bin": {
7
7
  "scalix": "./bin/scalix.js"
8
8
  },
9
+ "files": [
10
+ "dist",
11
+ "bin",
12
+ "CHANGELOG.md",
13
+ ".env.example"
14
+ ],
9
15
  "scripts": {
10
16
  "start": "tsx src/cli.tsx",
11
17
  "start:headless": "tsx src/headless.ts",
@@ -19,7 +25,11 @@
19
25
  "test:e2e": "vitest run --config vitest.config.e2e.ts",
20
26
  "check": "npm run typecheck && npm run lint && npm run test",
21
27
  "prepublishOnly": "npm run build",
22
- "release": "npm run build && node scripts/build-release.js"
28
+ "release": "npm run build && node scripts/build-release.js",
29
+ "bench": "tsx bench/src/main.ts",
30
+ "bench:live": "tsx bench/src/main.ts --live",
31
+ "typecheck:bench": "tsc --noEmit -p bench/tsconfig.json",
32
+ "bench:smoke-live": "tsx bench/src/smoke-live.ts"
23
33
  },
24
34
  "keywords": [
25
35
  "ai",
@@ -32,13 +42,9 @@
32
42
  ],
33
43
  "author": "Scalix World Private Limited",
34
44
  "license": "SEE LICENSE IN LICENSE",
35
- "repository": {
36
- "type": "git",
37
- "url": "https://github.com/scalixworld/scalix-coder.git"
38
- },
39
- "homepage": "https://scalix.world",
45
+ "homepage": "https://docs.scalix.world/coder",
40
46
  "bugs": {
41
- "url": "https://github.com/scalixworld/scalix-coder/issues"
47
+ "email": "coder@scalix.world"
42
48
  },
43
49
  "engines": {
44
50
  "node": ">=18"
package/PARITY-PLAN.md DELETED
@@ -1,116 +0,0 @@
1
- # Scalix Coder — Claude Code Parity Implementation Plan
2
-
3
- **Status:** In Progress
4
- **Target:** Full feature and UX parity with Claude Code
5
- **Estimated Total:** 12-15 days
6
-
7
- ---
8
-
9
- ## Phase 1: Security & Reliability (1-2 days) ✓ COMPLETE
10
-
11
- - [x] **1.1 Secure credential storage** — macOS Keychain / Linux libsecret via `keychain.ts`, auto-strips key from config file
12
- - [x] **1.2 Auto-retry with exponential backoff** — Already existed in agent.ts (backoffDelay + circuit breaker)
13
- - [x] **1.3 Non-streaming fallback** — `forceNonStreaming` flag auto-retries with `stream: false` on stream errors
14
-
15
- ## Phase 2: Chat Experience Parity (3-4 days) ✓ COMPLETE
16
-
17
- - [x] **2.1 Syntax highlighting in code blocks** — `cli-highlight` integration for 100+ languages + render cache (128 entries)
18
- - [x] **2.2 Collapsible tool groups** — Auto-groups 3+ consecutive tool items, Ctrl+O toggles expansion
19
- - [x] **2.3 Multiline input** — Alt+Enter inserts newline (terminal limitation prevents Shift+Enter)
20
- - [x] **2.4 "Always Allow" in permission prompts** — [A] key auto-allows tool for session, shown in prompt UI
21
- - [x] **2.5 @-mention typeahead in terminal** — `@` triggers fuzzy file search from project index, Tab/Enter selects
22
- - [x] **2.6 Ctrl+R reverse history search** — Incremental reverse search with visual indicator, Enter/Esc to accept/cancel
23
- - [x] **2.7 External editor support** — Ctrl+X Ctrl+E opens $EDITOR with input buffer, loads edited content back
24
- - [x] **2.8 Markdown render cache** — Content-hash cache (128 entries) prevents re-rendering identical content
25
- - [x] **2.9 Rate limit warning banner** — Context % in status bar when >50%, rate limit warning on 429 errors, 2-min auto-clear
26
- - [x] **2.10 Image paste from clipboard** — Ctrl+V detects clipboard images (macOS pngpaste/osascript, Linux xclip), shows `[Image #N]` chips, sends to vision model
27
-
28
- ## Phase 3: Permission & Hook System (2-3 days) ✓ COMPLETE
29
-
30
- - [x] **3.1 Scoped permissions** — `run_command(git:*)` prefix matching per tool, configurable allow/deny rules in `scopedPermissions` config
31
- - [x] **3.2 Enhanced hooks — 9 events** — Added Stop, SessionEnd, Notification events (now 9 total)
32
- - [x] **3.3 Prompt-type hooks** — `type: 'prompt'` hooks with `getPromptHooks()` API, separate from command hooks
33
- - [x] **3.4 Structured hook I/O** — JSON stdin with event/tool/args, JSON stdout parsing, exit code 2 = block with message
34
- - [x] **3.5 Matcher patterns** — Pipe-separated `run_command|edit_file`, wildcard `*`, regex `/pattern/` in hook match field
35
- - [x] **3.6 Permission memory** — "Always Allow" ([A] key) persisted per session, scoped rules in config, shown in `/permissions`
36
- - [x] **3.7 Recent denials view** — `/permissions` shows last 10 denied tools with timestamps and args
37
-
38
- ## Phase 4: Command & Skills System (2-3 days) ✓ COMPLETE
39
-
40
- - [x] **4.1 Rich command frontmatter** — Parse `allowed-tools`, `model`, `argument-hint`, `disable-model-invocation` from .md commands
41
- - [x] **4.2 @file references in commands** — `@path/to/file` includes file content inline in command prompt
42
- - [x] **4.3 Inline bash execution** — `` !`command` `` syntax executes shell and injects stdout into command prompt
43
- - [x] **4.4 Positional arguments** — `$1`, `$2`, etc. in addition to existing `$ARGS`
44
- - [x] **4.5 Skills framework** — `SKILL.md` with frontmatter (name, description, version, paths), progressive 3-level disclosure
45
- - [x] **4.6 Skill discovery** — `/skills` command lists available skills with descriptions
46
- - [x] **4.7 Path-triggered skills** — `paths:` frontmatter auto-loads skill when working in matching directories
47
- - [x] **4.8 Skill resources** — `scripts/`, `references/`, `examples/`, `assets/` directories per skill
48
- - [x] **4.9 Built-in skills** — feature-dev (7-phase workflow), code-review, commit-push-pr
49
-
50
- ## Phase 5: Session & Conversation (1-2 days) ✓ COMPLETE
51
-
52
- - [x] **5.1 Auto-save sessions** — Auto-saves crash state after each assistant turn via `saveCrashState()`
53
- - [x] **5.2 Session resume picker** — `/resume` tries crash recovery first, falls back to interactive session picker
54
- - [x] **5.3 Session forking** — `/fork [N]` branches conversation from message N in history
55
- - [x] **5.4 Session diff** — `getSessionDiff()` tracks file modifications (edit_file/write_file) per session
56
- - [x] **5.5 Crash recovery** — `.crash-recovery.json` auto-saved, detected on startup, resumable via `/resume`
57
-
58
- ## Phase 6: Configuration & Settings (1 day) ✓ COMPLETE
59
-
60
- - [x] **6.1 Local settings overrides** — `.scalix/settings.local.json` (gitignored) for per-project model/permission/limits overrides
61
- - [x] **6.2 Managed/organization settings** — `managed-settings.json` for enterprise policy enforcement (permission mode, security, scoped permissions)
62
- - [x] **6.3 Keybinding customization** — Covered by existing keyboard shortcuts system + Ctrl+X Ctrl+E editor support
63
- - [x] **6.4 Theme support** — Covered by existing marked-terminal + cli-highlight integration (auto-adapts to terminal colors)
64
- - [x] **6.5 .scalixignore file** — `.scalixignore` and `.scalix/.ignore` files for excluding paths from project indexing
65
-
66
- ## Phase 7: Advanced Features (1-2 days) ✓ COMPLETE
67
-
68
- - [x] **7.1 Worktree support** — `--worktree` CLI flag + `/worktree` command (list/create/remove/cleanup), backed by existing `worktree.ts`
69
- - [x] **7.2 CJK/RTL text support** — `text-utils.ts` with CJK/RTL detection, double-width char handling, `wrapText()`, `applyRTLMarks()`
70
- - [x] **7.3 IME composition support** — `stringWidth()` for accurate cursor positioning with CJK chars, combining mark handling
71
- - [x] **7.4 Idle session nudge** — 75-minute idle timer suggests `/clear` or `/compact` when conversation has 10+ messages
72
- - [x] **7.5 Terminal notification passthrough** — `terminal-notify.ts` with iTerm2/Kitty/Ghostty/tmux OSC support, auto-notify on task completion
73
- - [x] **7.6 Plugin management commands** — `/plugin install/uninstall/enable/disable/list` with git clone + local symlink support
74
- - [x] **7.7 Installation channels** — Homebrew formula, curl install script, PowerShell installer (all in `scripts/`)
75
-
76
- ---
77
-
78
- ## Summary
79
-
80
- | Phase | Items | Est. Days | Priority |
81
- |-------|-------|-----------|----------|
82
- | 1. Security & Reliability | 3 | 1-2 | Critical |
83
- | 2. Chat Experience | 10 | 3-4 | Critical |
84
- | 3. Permission & Hooks | 7 | 2-3 | Critical |
85
- | 4. Command & Skills | 9 | 2-3 | Important |
86
- | 5. Session & Conversation | 5 | 1-2 | Important |
87
- | 6. Configuration | 5 | 1 | Important |
88
- | 7. Advanced Features | 7 | 1-2 | Nice to have |
89
- | **Total** | **46** | **12-15** | |
90
-
91
- ---
92
-
93
- ## Already Ahead (Preserve These Advantages)
94
-
95
- These features already exceed Claude Code — protect them during implementation:
96
-
97
- - 40+ built-in tools (browser automation, multi-provider search, vision, voice)
98
- - Model-agnostic with multi-model complexity routing
99
- - Dynamic model discovery from API
100
- - Auto-convention detection and project memory
101
- - Injection detection with configurable sensitivity
102
- - Checkpoint/undo system (`/checkpoint`, `/undo`)
103
- - VS Code: inline chat, tab completion, context menu, 6 @-mention types
104
- - Session replay (`/replay`)
105
- - Message queueing during processing
106
- - Shell escape (`!cmd`) prefix
107
- - Input stashing (Ctrl+S)
108
- - Auto-verification after file edits (build/lint check)
109
- - Multiple output formats (text, json, json-schema, patch, markdown)
110
- - Gradient banner with ink-big-text
111
- - Informative status bar (model + tokens + tools + mode + elapsed time + version)
112
- - Box-drawn diff frames with addition/deletion summary
113
- - Smart error hints (pattern-matched suggestions)
114
- - Scroll indicators (↑ more above / ↓ more below)
115
- - Distinctive ❯ prompt with spinner animation
116
- - Dynamic placeholder text