@sayknow-cli/coding-agent 0.5.2 → 0.5.8
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.
- package/CHANGELOG.md +66 -16
- package/dist/types/cli/update-cli.d.ts +8 -1
- package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +73 -0
- package/dist/types/dap/client.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +5 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +13 -1
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +8 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +9 -0
- package/dist/types/session/session-storage.d.ts +17 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +22 -0
- package/dist/types/session-import/command.d.ts +20 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +28 -0
- package/dist/types/session-import/types.d.ts +125 -0
- package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
- package/dist/types/slash-commands/types.d.ts +5 -0
- package/dist/types/system-prompt.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +5 -0
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +13 -7
- package/scripts/compile-args.ts +8 -4
- package/scripts/generate-sdk-operation-inventory.ts +5 -6
- package/src/cli/config-cli.ts +23 -8
- package/src/cli/update-cli.ts +265 -21
- package/src/config/atomic-yaml-patch.ts +167 -30
- package/src/config/keybindings.ts +10 -0
- package/src/config/model-profiles.ts +84 -16
- package/src/config/model-registry.ts +60 -38
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +79 -0
- package/src/config/settings.ts +13 -2
- package/src/dap/client.ts +78 -30
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/lsp/client.ts +17 -29
- package/src/modes/action-registry.ts +2 -0
- package/src/modes/components/welcome.ts +5 -0
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +71 -6
- package/src/modes/controllers/plan-mode-controller.ts +8 -1
- package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
- package/src/modes/controllers/selector-controller.ts +47 -19
- package/src/modes/interactive-mode.ts +54 -4
- package/src/modes/types.ts +5 -1
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/runtime-mcp/transports/stdio.ts +96 -33
- package/src/sdk/broker/lifecycle.ts +6 -5
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +27 -18
- package/src/session/agent-session.ts +155 -15
- package/src/session/internal/managed-session-scope.ts +41 -6
- package/src/session/internal/managed-session-storage.ts +14 -0
- package/src/session/session-manager.ts +113 -11
- package/src/session/session-storage.ts +70 -0
- package/src/session-import/claude.ts +382 -0
- package/src/session-import/codex.ts +458 -0
- package/src/session-import/command.ts +61 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +27 -0
- package/src/session-import/redact.ts +125 -0
- package/src/session-import/service.ts +749 -0
- package/src/session-import/types.ts +163 -0
- package/src/skc-runtime/launch-worktree.ts +277 -4
- package/src/slash-commands/acp-builtins.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +100 -1
- package/src/slash-commands/types.ts +5 -0
- package/src/system-prompt.ts +7 -0
- package/src/tools/ask.ts +5 -0
- package/src/tools/fetch.ts +7 -2
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- package/src/export/html/template.css +0 -1060
- package/src/export/html/template.html +0 -47
- package/src/export/html/template.js +0 -2348
- package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
- package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
- package/vendor/insane-search/engine/tests/test_u1.py +0 -200
- package/vendor/insane-search/engine/tests/test_u4.py +0 -131
- package/vendor/insane-search/engine/tests/test_u5.py +0 -163
- package/vendor/insane-search/engine/tests/test_u7.py +0 -124
- package/vendor/insane-search/engine/tests/test_u8.py +0 -216
|
@@ -93,7 +93,31 @@ export class AtomicYamlReplaceError extends Error {
|
|
|
93
93
|
this.name = "AtomicYamlReplaceError";
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
/** Raised when the configured lexical route no longer resolves to its original target. */
|
|
97
|
+
export class AtomicYamlRetargetError extends Error {
|
|
98
|
+
readonly code = "ATOMIC_YAML_RETARGETED";
|
|
96
99
|
|
|
100
|
+
constructor() {
|
|
101
|
+
super("Atomic YAML configuration target changed before publication.");
|
|
102
|
+
this.name = "AtomicYamlRetargetError";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Test-only coordination hooks for deterministic target-authority races. */
|
|
107
|
+
export const AtomicYamlTestHooks: {
|
|
108
|
+
afterTargetBound?: (canonicalPath: string) => Promise<void> | void;
|
|
109
|
+
} = {};
|
|
110
|
+
|
|
111
|
+
type AtomicYamlTargetIdentity = {
|
|
112
|
+
device: bigint;
|
|
113
|
+
inode: bigint;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type AtomicYamlTarget = {
|
|
117
|
+
canonicalPath: string;
|
|
118
|
+
resolvedPath: string;
|
|
119
|
+
identity: AtomicYamlTargetIdentity | undefined;
|
|
120
|
+
};
|
|
97
121
|
type PathState = { exists: boolean; value: unknown };
|
|
98
122
|
type ReceiptChange = {
|
|
99
123
|
path: string;
|
|
@@ -103,6 +127,7 @@ type ReceiptChange = {
|
|
|
103
127
|
};
|
|
104
128
|
|
|
105
129
|
const queues = new Map<string, Promise<void>>();
|
|
130
|
+
const trustedQueuedTargets = new Map<string, AtomicYamlTarget>();
|
|
106
131
|
let nextReceiptRevision = 0;
|
|
107
132
|
/** Bounded Windows sharing-violation retries: 10, 25, 50, 100, then 200 ms. */
|
|
108
133
|
const WINDOWS_RENAME_BACKOFF_MS = [10, 25, 50, 100, 200] as const;
|
|
@@ -112,6 +137,80 @@ function canonicalConfigPath(configPath: string): string {
|
|
|
112
137
|
return path.normalize(path.resolve(configPath));
|
|
113
138
|
}
|
|
114
139
|
|
|
140
|
+
async function resolveMissingAtomicYamlPath(inputPath: string): Promise<string> {
|
|
141
|
+
let unresolvedPath = inputPath;
|
|
142
|
+
const missingSegments: string[] = [];
|
|
143
|
+
for (;;) {
|
|
144
|
+
try {
|
|
145
|
+
return path.join(await fs.realpath(unresolvedPath), ...missingSegments.reverse());
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
148
|
+
const parent = path.dirname(unresolvedPath);
|
|
149
|
+
if (parent === unresolvedPath) throw error;
|
|
150
|
+
missingSegments.push(path.basename(unresolvedPath));
|
|
151
|
+
unresolvedPath = parent;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function resolveAtomicYamlTargetPath(inputPath: string, depth = 0): Promise<string> {
|
|
157
|
+
const canonicalPath = canonicalConfigPath(inputPath);
|
|
158
|
+
try {
|
|
159
|
+
return await fs.realpath(canonicalPath);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const metadata = await fs.lstat(canonicalPath);
|
|
165
|
+
if (metadata.isSymbolicLink()) {
|
|
166
|
+
if (depth >= 32) throw new AtomicYamlRetargetError();
|
|
167
|
+
const linkedPath = await fs.readlink(canonicalPath);
|
|
168
|
+
const targetPath = path.isAbsolute(linkedPath)
|
|
169
|
+
? linkedPath
|
|
170
|
+
: path.resolve(path.dirname(canonicalPath), linkedPath);
|
|
171
|
+
return await resolveAtomicYamlTargetPath(targetPath, depth + 1);
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
175
|
+
}
|
|
176
|
+
return await resolveMissingAtomicYamlPath(canonicalPath);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function readAtomicYamlTargetIdentity(resolvedPath: string): Promise<AtomicYamlTargetIdentity | undefined> {
|
|
180
|
+
try {
|
|
181
|
+
const metadata = await fs.stat(resolvedPath, { bigint: true });
|
|
182
|
+
return { device: metadata.dev, inode: metadata.ino };
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function resolveAtomicYamlTarget(configPath: string): Promise<AtomicYamlTarget> {
|
|
190
|
+
const canonicalPath = canonicalConfigPath(configPath);
|
|
191
|
+
const resolvedPath = await resolveAtomicYamlTargetPath(canonicalPath);
|
|
192
|
+
return {
|
|
193
|
+
canonicalPath,
|
|
194
|
+
resolvedPath,
|
|
195
|
+
identity: await readAtomicYamlTargetIdentity(resolvedPath),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sameAtomicYamlIdentity(
|
|
200
|
+
left: AtomicYamlTargetIdentity | undefined,
|
|
201
|
+
right: AtomicYamlTargetIdentity | undefined,
|
|
202
|
+
): boolean {
|
|
203
|
+
if (!left || !right) return left === right;
|
|
204
|
+
return left.device === right.device && left.inode === right.inode;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function assertAtomicYamlTarget(target: AtomicYamlTarget): Promise<void> {
|
|
208
|
+
const current = await resolveAtomicYamlTarget(target.canonicalPath);
|
|
209
|
+
if (current.resolvedPath !== target.resolvedPath || !sameAtomicYamlIdentity(current.identity, target.identity)) {
|
|
210
|
+
throw new AtomicYamlRetargetError();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
115
214
|
function assertPatch(patch: AtomicYamlPatch): void {
|
|
116
215
|
if (
|
|
117
216
|
!patch ||
|
|
@@ -266,12 +365,12 @@ async function replaceWithRetry(tempPath: string, configPath: string, options: A
|
|
|
266
365
|
}
|
|
267
366
|
|
|
268
367
|
async function writeAtomicYaml(
|
|
269
|
-
|
|
368
|
+
target: AtomicYamlTarget,
|
|
270
369
|
value: Record<string, unknown>,
|
|
271
370
|
options: AtomicYamlPatchOptions,
|
|
272
371
|
): Promise<void> {
|
|
273
|
-
const directory = path.dirname(
|
|
274
|
-
const tempPath = path.join(directory, `.${path.basename(
|
|
372
|
+
const directory = path.dirname(target.resolvedPath);
|
|
373
|
+
const tempPath = path.join(directory, `.${path.basename(target.resolvedPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
275
374
|
try {
|
|
276
375
|
const tempHandle = await fs.open(tempPath, "wx", 0o600);
|
|
277
376
|
try {
|
|
@@ -280,8 +379,12 @@ async function writeAtomicYaml(
|
|
|
280
379
|
} finally {
|
|
281
380
|
await tempHandle.close();
|
|
282
381
|
}
|
|
283
|
-
await
|
|
382
|
+
await assertAtomicYamlTarget(target);
|
|
383
|
+
await replaceWithRetry(tempPath, target.resolvedPath, options);
|
|
284
384
|
await syncParentDirectory(directory);
|
|
385
|
+
const publishedTarget = await resolveAtomicYamlTarget(target.canonicalPath);
|
|
386
|
+
if (publishedTarget.resolvedPath !== target.resolvedPath) throw new AtomicYamlRetargetError();
|
|
387
|
+
target.identity = publishedTarget.identity;
|
|
285
388
|
} finally {
|
|
286
389
|
await fs.rm(tempPath, { force: true }).catch(() => undefined);
|
|
287
390
|
}
|
|
@@ -302,10 +405,12 @@ function createReceipt(
|
|
|
302
405
|
},
|
|
303
406
|
async restore(): Promise<CasRestoreResult> {
|
|
304
407
|
if (discarded) return { status: "discarded" };
|
|
305
|
-
return await enqueueAtomicYamlOperation(configPath, async
|
|
408
|
+
return await enqueueAtomicYamlOperation(configPath, async (_canonicalPath, target) => {
|
|
306
409
|
if (discarded) return { status: "discarded" };
|
|
307
|
-
|
|
308
|
-
|
|
410
|
+
await assertAtomicYamlTarget(target);
|
|
411
|
+
return await withFileLock(target.resolvedPath, async () => {
|
|
412
|
+
await assertAtomicYamlTarget(target);
|
|
413
|
+
const { current, root } = await readYaml(target.resolvedPath);
|
|
309
414
|
const conflicts = changes
|
|
310
415
|
.filter(
|
|
311
416
|
change =>
|
|
@@ -320,7 +425,7 @@ function createReceipt(
|
|
|
320
425
|
: { path: change.path, op: "unset" },
|
|
321
426
|
);
|
|
322
427
|
await options.validateRoot?.(root, restorePatches);
|
|
323
|
-
const receipt = await applyPatchesUnderLock(
|
|
428
|
+
const receipt = await applyPatchesUnderLock(target, current, restorePatches, options);
|
|
324
429
|
await options.onRestored?.(restorePatches);
|
|
325
430
|
return { status: "restored", receipt };
|
|
326
431
|
});
|
|
@@ -330,12 +435,12 @@ function createReceipt(
|
|
|
330
435
|
}
|
|
331
436
|
|
|
332
437
|
async function applyPatchesUnderLock(
|
|
333
|
-
|
|
438
|
+
target: AtomicYamlTarget,
|
|
334
439
|
current: Record<string, unknown>,
|
|
335
440
|
patches: readonly AtomicYamlPatch[],
|
|
336
441
|
options: AtomicYamlPatchOptions,
|
|
337
442
|
): Promise<CasReceipt> {
|
|
338
|
-
if (patches.length === 0) return createReceipt(
|
|
443
|
+
if (patches.length === 0) return createReceipt(target.canonicalPath, [], options);
|
|
339
444
|
|
|
340
445
|
for (const patch of patches) {
|
|
341
446
|
if (!patch.expected) continue;
|
|
@@ -373,8 +478,8 @@ async function applyPatchesUnderLock(
|
|
|
373
478
|
}
|
|
374
479
|
const changes = [...changesByPath.values()];
|
|
375
480
|
|
|
376
|
-
await writeAtomicYaml(
|
|
377
|
-
return createReceipt(
|
|
481
|
+
await writeAtomicYaml(target, current, options);
|
|
482
|
+
return createReceipt(target.canonicalPath, changes, options);
|
|
378
483
|
}
|
|
379
484
|
|
|
380
485
|
/** Build patches from current durable YAML while holding the shared queue and file lock. */
|
|
@@ -385,14 +490,17 @@ export function applyAtomicYamlPatchesWithCurrent(
|
|
|
385
490
|
) => Promise<readonly AtomicYamlPatch[]> | readonly AtomicYamlPatch[],
|
|
386
491
|
options: AtomicYamlPatchOptions = {},
|
|
387
492
|
): Promise<CasReceipt> {
|
|
388
|
-
return enqueueAtomicYamlOperation(configPath, async
|
|
389
|
-
await
|
|
390
|
-
|
|
391
|
-
|
|
493
|
+
return enqueueAtomicYamlOperation(configPath, async (_canonicalPath, target) => {
|
|
494
|
+
await assertAtomicYamlTarget(target);
|
|
495
|
+
await fs.mkdir(path.dirname(target.resolvedPath), { recursive: true, mode: 0o700 });
|
|
496
|
+
await assertAtomicYamlTarget(target);
|
|
497
|
+
return await withFileLock(target.resolvedPath, async () => {
|
|
498
|
+
await assertAtomicYamlTarget(target);
|
|
499
|
+
const { current, root } = await readYaml(target.resolvedPath);
|
|
392
500
|
const patches = await buildPatches(current);
|
|
393
501
|
for (const patch of patches) assertPatch(patch);
|
|
394
502
|
await options.validateRoot?.(root, patches);
|
|
395
|
-
return await applyPatchesUnderLock(
|
|
503
|
+
return await applyPatchesUnderLock(target, current, patches, options);
|
|
396
504
|
});
|
|
397
505
|
});
|
|
398
506
|
}
|
|
@@ -404,18 +512,41 @@ export function applyAtomicYamlPatchesWithCurrent(
|
|
|
404
512
|
*/
|
|
405
513
|
export function enqueueAtomicYamlOperation<T>(
|
|
406
514
|
configPath: string,
|
|
407
|
-
operation: (canonicalConfigPath: string) => Promise<T>,
|
|
515
|
+
operation: (canonicalConfigPath: string, target: AtomicYamlTarget) => Promise<T>,
|
|
408
516
|
): Promise<T> {
|
|
409
517
|
const canonicalPath = canonicalConfigPath(configPath);
|
|
410
518
|
const prior = queues.get(canonicalPath) ?? Promise.resolve();
|
|
411
|
-
const
|
|
519
|
+
const targetAtEnqueue = resolveAtomicYamlTarget(canonicalPath).then(async target => {
|
|
520
|
+
await AtomicYamlTestHooks.afterTargetBound?.(canonicalPath);
|
|
521
|
+
return target;
|
|
522
|
+
});
|
|
523
|
+
const result = prior
|
|
524
|
+
.catch(() => undefined)
|
|
525
|
+
.then(async () => {
|
|
526
|
+
const target = await targetAtEnqueue;
|
|
527
|
+
const current = await resolveAtomicYamlTarget(canonicalPath);
|
|
528
|
+
if (
|
|
529
|
+
current.resolvedPath !== target.resolvedPath ||
|
|
530
|
+
(!sameAtomicYamlIdentity(current.identity, target.identity) &&
|
|
531
|
+
!sameAtomicYamlIdentity(trustedQueuedTargets.get(canonicalPath)?.identity, current.identity))
|
|
532
|
+
) {
|
|
533
|
+
throw new AtomicYamlRetargetError();
|
|
534
|
+
}
|
|
535
|
+
target.identity = current.identity;
|
|
536
|
+
const value = await operation(canonicalPath, target);
|
|
537
|
+
trustedQueuedTargets.set(canonicalPath, { ...target });
|
|
538
|
+
return value;
|
|
539
|
+
});
|
|
412
540
|
const completion = result.then(
|
|
413
541
|
() => undefined,
|
|
414
542
|
() => undefined,
|
|
415
543
|
);
|
|
416
544
|
queues.set(canonicalPath, completion);
|
|
417
545
|
void completion.finally(() => {
|
|
418
|
-
if (queues.get(canonicalPath) === completion)
|
|
546
|
+
if (queues.get(canonicalPath) === completion) {
|
|
547
|
+
queues.delete(canonicalPath);
|
|
548
|
+
trustedQueuedTargets.delete(canonicalPath);
|
|
549
|
+
}
|
|
419
550
|
});
|
|
420
551
|
return result;
|
|
421
552
|
}
|
|
@@ -429,14 +560,17 @@ export function reserveAtomicYamlPatchSlot(
|
|
|
429
560
|
patches: () => Promise<readonly AtomicYamlPatch[]> | readonly AtomicYamlPatch[],
|
|
430
561
|
options: AtomicYamlPatchOptions = {},
|
|
431
562
|
): Promise<CasReceipt> {
|
|
432
|
-
return enqueueAtomicYamlOperation(configPath, async
|
|
563
|
+
return enqueueAtomicYamlOperation(configPath, async (_canonicalPath, target) => {
|
|
433
564
|
const nextPatches = await patches();
|
|
434
565
|
for (const patch of nextPatches) assertPatch(patch);
|
|
435
|
-
await
|
|
436
|
-
|
|
437
|
-
|
|
566
|
+
await assertAtomicYamlTarget(target);
|
|
567
|
+
await fs.mkdir(path.dirname(target.resolvedPath), { recursive: true, mode: 0o700 });
|
|
568
|
+
await assertAtomicYamlTarget(target);
|
|
569
|
+
return await withFileLock(target.resolvedPath, async () => {
|
|
570
|
+
await assertAtomicYamlTarget(target);
|
|
571
|
+
const { current, root } = await readYaml(target.resolvedPath);
|
|
438
572
|
await options.validateRoot?.(root, nextPatches);
|
|
439
|
-
return await applyPatchesUnderLock(
|
|
573
|
+
return await applyPatchesUnderLock(target, current, nextPatches, options);
|
|
440
574
|
});
|
|
441
575
|
});
|
|
442
576
|
}
|
|
@@ -450,14 +584,17 @@ export function reserveAtomicYamlUpdateSlot<T>(
|
|
|
450
584
|
update: () => Promise<AtomicYamlUpdate<T>> | AtomicYamlUpdate<T>,
|
|
451
585
|
options: AtomicYamlPatchOptions = {},
|
|
452
586
|
): Promise<T> {
|
|
453
|
-
return enqueueAtomicYamlOperation(configPath, async
|
|
587
|
+
return enqueueAtomicYamlOperation(configPath, async (_canonicalPath, target) => {
|
|
454
588
|
const atomicUpdate = await update();
|
|
455
|
-
await
|
|
456
|
-
|
|
457
|
-
|
|
589
|
+
await assertAtomicYamlTarget(target);
|
|
590
|
+
await fs.mkdir(path.dirname(target.resolvedPath), { recursive: true, mode: 0o700 });
|
|
591
|
+
await assertAtomicYamlTarget(target);
|
|
592
|
+
return await withFileLock(target.resolvedPath, async () => {
|
|
593
|
+
await assertAtomicYamlTarget(target);
|
|
594
|
+
const { current } = await readYaml(target.resolvedPath);
|
|
458
595
|
const result = await atomicUpdate.apply(current);
|
|
459
596
|
if (atomicUpdate.shouldWrite?.(result) !== false) {
|
|
460
|
-
await writeAtomicYaml(
|
|
597
|
+
await writeAtomicYaml(target, current, options);
|
|
461
598
|
}
|
|
462
599
|
await atomicUpdate.committed?.(current, result);
|
|
463
600
|
return result;
|
|
@@ -29,6 +29,7 @@ interface AppKeybindings {
|
|
|
29
29
|
"app.model.select": true;
|
|
30
30
|
"app.model.selectTemporary": true;
|
|
31
31
|
"app.tools.expand": true;
|
|
32
|
+
"app.todo.toggle": true;
|
|
32
33
|
"app.tool.backgroundFold": true;
|
|
33
34
|
"app.editor.external": true;
|
|
34
35
|
"app.message.followUp": true;
|
|
@@ -37,6 +38,7 @@ interface AppKeybindings {
|
|
|
37
38
|
"app.clipboard.pasteImage": true;
|
|
38
39
|
"app.clipboard.copyLine": true;
|
|
39
40
|
"app.clipboard.copyPrompt": true;
|
|
41
|
+
"app.oauth.copyUrl": true;
|
|
40
42
|
"app.session.new": true;
|
|
41
43
|
"app.session.tree": true;
|
|
42
44
|
"app.session.fork": true;
|
|
@@ -131,6 +133,10 @@ export const KEYBINDINGS = {
|
|
|
131
133
|
defaultKeys: "ctrl+o",
|
|
132
134
|
description: "Expand tools",
|
|
133
135
|
},
|
|
136
|
+
"app.todo.toggle": {
|
|
137
|
+
defaultKeys: "alt+shift+t",
|
|
138
|
+
description: "Toggle todo list expansion",
|
|
139
|
+
},
|
|
134
140
|
"app.tool.backgroundFold": {
|
|
135
141
|
defaultKeys: "ctrl+b",
|
|
136
142
|
description: "Fold/background supported foreground tool",
|
|
@@ -163,6 +169,10 @@ export const KEYBINDINGS = {
|
|
|
163
169
|
defaultKeys: "alt+shift+c",
|
|
164
170
|
description: "Copy prompt",
|
|
165
171
|
},
|
|
172
|
+
"app.oauth.copyUrl": {
|
|
173
|
+
defaultKeys: "alt+shift+u",
|
|
174
|
+
description: "Copy OAuth authorization URL",
|
|
175
|
+
},
|
|
166
176
|
"app.session.new": {
|
|
167
177
|
defaultKeys: "ctrl+n",
|
|
168
178
|
description: "Start a new session",
|
|
@@ -92,6 +92,41 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
92
92
|
critic: "openai-codex/gpt-5.6-sol:max",
|
|
93
93
|
architect: "openai-codex/gpt-5.6-sol:xhigh",
|
|
94
94
|
}),
|
|
95
|
+
profile("macos-omlx-fast", ["omlx"], {
|
|
96
|
+
default: "omlx/Qwen3.6-35B-A3B-4bit:low",
|
|
97
|
+
executor: "omlx/Qwen3.6-35B-A3B-4bit:low",
|
|
98
|
+
architect: "omlx/Qwen3.6-35B-A3B-4bit:high",
|
|
99
|
+
planner: "omlx/Qwen3.6-35B-A3B-4bit:medium",
|
|
100
|
+
critic: "omlx/Qwen3.6-35B-A3B-4bit:high",
|
|
101
|
+
}),
|
|
102
|
+
profile("macos-omlx-balanced", ["omlx"], {
|
|
103
|
+
default: "omlx/Qwen3.6-35B-A3B-8bit:low",
|
|
104
|
+
executor: "omlx/Qwen3.6-35B-A3B-8bit:low",
|
|
105
|
+
architect: "omlx/Qwen3.6-35B-A3B-8bit:high",
|
|
106
|
+
planner: "omlx/Qwen3.6-35B-A3B-8bit:medium",
|
|
107
|
+
critic: "omlx/Qwen3.6-35B-A3B-8bit:high",
|
|
108
|
+
}),
|
|
109
|
+
profile("macos-omlx-quality", ["omlx"], {
|
|
110
|
+
default: "omlx/Qwen3.6-35B-A3B-8bit:low",
|
|
111
|
+
executor: "omlx/Qwen3.6-35B-A3B-8bit:low",
|
|
112
|
+
architect: "omlx/Qwen3.6-35B-A3B-8bit:high",
|
|
113
|
+
planner: "omlx/Qwen3.6-35B-A3B-8bit:medium",
|
|
114
|
+
critic: "omlx/Qwen3.8-27B-8bit:high",
|
|
115
|
+
}),
|
|
116
|
+
profile("macos-omlx-abliterated-fast", ["omlx"], {
|
|
117
|
+
default: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:low",
|
|
118
|
+
executor: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:low",
|
|
119
|
+
architect: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:high",
|
|
120
|
+
planner: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:medium",
|
|
121
|
+
critic: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:high",
|
|
122
|
+
}),
|
|
123
|
+
profile("macos-omlx-abliterated-balanced", ["omlx"], {
|
|
124
|
+
default: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:low",
|
|
125
|
+
executor: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:low",
|
|
126
|
+
architect: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:high",
|
|
127
|
+
planner: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:medium",
|
|
128
|
+
critic: "omlx/Qwen3.8-27B-Uncensored-MLX-4bit:high",
|
|
129
|
+
}),
|
|
95
130
|
profile("opencodego", ["opencode-go"], {
|
|
96
131
|
default: "opencode-go/kimi-k2.6",
|
|
97
132
|
executor: "opencode-go/deepseek-v4-flash",
|
|
@@ -114,25 +149,25 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
114
149
|
architect: "anthropic/claude-fable-5:xhigh",
|
|
115
150
|
}),
|
|
116
151
|
profile("glm-eco", ["zai"], {
|
|
117
|
-
default: "zai/glm-5.
|
|
118
|
-
executor: "zai/glm-5.
|
|
119
|
-
planner: "zai/glm-5.
|
|
120
|
-
critic: "zai/glm-5.
|
|
121
|
-
architect: "zai/glm-5.
|
|
152
|
+
default: "zai/glm-5.3:low",
|
|
153
|
+
executor: "zai/glm-5.3:minimal",
|
|
154
|
+
planner: "zai/glm-5.3:low",
|
|
155
|
+
critic: "zai/glm-5.3:medium",
|
|
156
|
+
architect: "zai/glm-5.3:high",
|
|
122
157
|
}),
|
|
123
158
|
profile("glm-medium", ["zai"], {
|
|
124
|
-
default: "zai/glm-5.
|
|
125
|
-
executor: "zai/glm-5.
|
|
126
|
-
planner: "zai/glm-5.
|
|
127
|
-
critic: "zai/glm-5.
|
|
128
|
-
architect: "zai/glm-5.
|
|
159
|
+
default: "zai/glm-5.3:medium",
|
|
160
|
+
executor: "zai/glm-5.3:low",
|
|
161
|
+
planner: "zai/glm-5.3:medium",
|
|
162
|
+
critic: "zai/glm-5.3:high",
|
|
163
|
+
architect: "zai/glm-5.3:xhigh",
|
|
129
164
|
}),
|
|
130
165
|
profile("glm-pro", ["zai"], {
|
|
131
|
-
default: "zai/glm-5.
|
|
132
|
-
executor: "zai/glm-5.
|
|
133
|
-
planner: "zai/glm-5.
|
|
134
|
-
critic: "zai/glm-5.
|
|
135
|
-
architect: "zai/glm-5.
|
|
166
|
+
default: "zai/glm-5.3:xhigh",
|
|
167
|
+
executor: "zai/glm-5.3:medium",
|
|
168
|
+
planner: "zai/glm-5.3:high",
|
|
169
|
+
critic: "zai/glm-5.3:xhigh",
|
|
170
|
+
architect: "zai/glm-5.3:xhigh",
|
|
136
171
|
}),
|
|
137
172
|
profile("kimi-coding-plan-eco", ["kimi-code"], {
|
|
138
173
|
default: "kimi-code/k3:low",
|
|
@@ -327,6 +362,23 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
|
|
|
327
362
|
"codex-eco": { displayName: "Codex Eco", providerGroup: "CODEX" },
|
|
328
363
|
"codex-medium": { displayName: "Codex Medium", providerGroup: "CODEX" },
|
|
329
364
|
"codex-pro": { displayName: "Codex Pro", providerGroup: "CODEX" },
|
|
365
|
+
"macos-omlx-fast": { displayName: "4-bit Fast (MoE measured 93.5 tok/s)", providerGroup: "macOS Local (oMLX)" },
|
|
366
|
+
"macos-omlx-balanced": {
|
|
367
|
+
displayName: "8-bit Balanced (MoE measured 71.1 tok/s)",
|
|
368
|
+
providerGroup: "macOS Local (oMLX)",
|
|
369
|
+
},
|
|
370
|
+
"macos-omlx-quality": {
|
|
371
|
+
displayName: "Quality mix (8-bit MoE + 8-bit dense critic)",
|
|
372
|
+
providerGroup: "macOS Local (oMLX)",
|
|
373
|
+
},
|
|
374
|
+
"macos-omlx-abliterated-fast": {
|
|
375
|
+
displayName: "Uncensored 4-bit Fast (measured 19.8 tok/s)",
|
|
376
|
+
providerGroup: "macOS Local (oMLX)",
|
|
377
|
+
},
|
|
378
|
+
"macos-omlx-abliterated-balanced": {
|
|
379
|
+
displayName: "Uncensored 4-bit Balanced (same winner as fast)",
|
|
380
|
+
providerGroup: "macOS Local (oMLX)",
|
|
381
|
+
},
|
|
330
382
|
opencodego: { displayName: "OpenCodeGo", providerGroup: "OPENCODEGO" },
|
|
331
383
|
"claude-opus": { displayName: "Claude Opus", providerGroup: "CLAUDE" },
|
|
332
384
|
"claude-fable": { displayName: "Claude Fable", providerGroup: "CLAUDE" },
|
|
@@ -361,6 +413,7 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
|
|
|
361
413
|
|
|
362
414
|
const PROFILE_GROUP_ORDER = [
|
|
363
415
|
"CODEX",
|
|
416
|
+
"macOS Local (oMLX)",
|
|
364
417
|
"OPENCODEGO",
|
|
365
418
|
"CLAUDE",
|
|
366
419
|
"GLM",
|
|
@@ -372,6 +425,14 @@ const PROFILE_GROUP_ORDER = [
|
|
|
372
425
|
"ALIBABA TOKEN PLAN",
|
|
373
426
|
"COMBOS",
|
|
374
427
|
];
|
|
428
|
+
const MACOS_OMLX_PROFILE_ORDER = [
|
|
429
|
+
"macos-omlx-fast",
|
|
430
|
+
"macos-omlx-balanced",
|
|
431
|
+
"macos-omlx-quality",
|
|
432
|
+
"macos-omlx-abliterated-fast",
|
|
433
|
+
"macos-omlx-abliterated-balanced",
|
|
434
|
+
] as const;
|
|
435
|
+
const MACOS_OMLX_PROFILE_RANK = new Map<string, number>(MACOS_OMLX_PROFILE_ORDER.map((name, index) => [name, index]));
|
|
375
436
|
|
|
376
437
|
const PROFILE_RECOMMENDATIONS: Record<string, string> = {
|
|
377
438
|
"openai-codex": "codex-medium",
|
|
@@ -386,6 +447,7 @@ const PROFILE_RECOMMENDATIONS: Record<string, string> = {
|
|
|
386
447
|
xai: "grok-medium",
|
|
387
448
|
"grok-build": "grok-build-pro",
|
|
388
449
|
cursor: "cursor-medium",
|
|
450
|
+
omlx: "macos-omlx-balanced",
|
|
389
451
|
"minimax-code": "minimax-medium",
|
|
390
452
|
"alibaba-token-plan": "alibaba-token-plan-balanced",
|
|
391
453
|
};
|
|
@@ -420,7 +482,13 @@ export function groupModelProfilesForPresetLanding(
|
|
|
420
482
|
}
|
|
421
483
|
for (const [group, entries] of groups) {
|
|
422
484
|
if (entries.length === 0) groups.delete(group);
|
|
423
|
-
else
|
|
485
|
+
else if (group === "macOS Local (oMLX)") {
|
|
486
|
+
entries.sort(
|
|
487
|
+
(a, b) =>
|
|
488
|
+
(MACOS_OMLX_PROFILE_RANK.get(a.name) ?? Number.MAX_SAFE_INTEGER) -
|
|
489
|
+
(MACOS_OMLX_PROFILE_RANK.get(b.name) ?? Number.MAX_SAFE_INTEGER),
|
|
490
|
+
);
|
|
491
|
+
} else entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
424
492
|
}
|
|
425
493
|
return groups;
|
|
426
494
|
}
|
|
@@ -7,7 +7,9 @@ import {
|
|
|
7
7
|
type CacheRetention,
|
|
8
8
|
type Context,
|
|
9
9
|
createModelManager,
|
|
10
|
+
Effort,
|
|
10
11
|
enrichModelThinking,
|
|
12
|
+
fetchOpenAICompatibleModels,
|
|
11
13
|
getBundledModels,
|
|
12
14
|
getBundledProviders,
|
|
13
15
|
googleAntigravityModelManagerOptions,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
type ModelRequestTransform,
|
|
19
21
|
openaiCodexModelManagerOptions,
|
|
20
22
|
PROVIDER_DESCRIPTORS,
|
|
23
|
+
type Provider,
|
|
21
24
|
readModelCache,
|
|
22
25
|
registerCustomApi,
|
|
23
26
|
type SimpleStreamOptions,
|
|
@@ -597,6 +600,10 @@ function toPositiveNumberOrUndefined(value: unknown): number | undefined {
|
|
|
597
600
|
return undefined;
|
|
598
601
|
}
|
|
599
602
|
|
|
603
|
+
function toSafePositiveIntegerOrUndefined(value: unknown): number | undefined {
|
|
604
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
605
|
+
}
|
|
606
|
+
|
|
600
607
|
function extractOllamaContextWindow(payload: Record<string, unknown>): number | undefined {
|
|
601
608
|
const modelInfo = payload.model_info;
|
|
602
609
|
if (isRecord(modelInfo)) {
|
|
@@ -1905,6 +1912,8 @@ export class ModelRegistry {
|
|
|
1905
1912
|
case "llama.cpp":
|
|
1906
1913
|
return this.#discoverLlamaCppModels(providerConfig);
|
|
1907
1914
|
case "lm-studio":
|
|
1915
|
+
case "omlx":
|
|
1916
|
+
case "sglang":
|
|
1908
1917
|
case "openai-models-list":
|
|
1909
1918
|
return this.#discoverOpenAIModelsList(providerConfig);
|
|
1910
1919
|
}
|
|
@@ -2210,48 +2219,61 @@ export class ModelRegistry {
|
|
|
2210
2219
|
|
|
2211
2220
|
async #discoverOpenAIModelsList(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
|
|
2212
2221
|
const baseUrl = this.#normalizeOpenAIModelsListBaseUrl(providerConfig.baseUrl);
|
|
2213
|
-
const modelsUrl = `${baseUrl}/models`;
|
|
2214
|
-
|
|
2215
2222
|
const headers: Record<string, string> = { ...(providerConfig.headers ?? {}) };
|
|
2216
2223
|
const apiKey = await this.authStorage.getApiKey(providerConfig.provider);
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2224
|
+
const bearerToken = apiKey && apiKey !== DEFAULT_LOCAL_TOKEN && apiKey !== kNoAuth ? apiKey : undefined;
|
|
2225
|
+
const isOmlx = providerConfig.provider === "omlx";
|
|
2226
|
+
const parseDiscoveryLimit = isOmlx ? toSafePositiveIntegerOrUndefined : toPositiveNumberOrUndefined;
|
|
2227
|
+
const discovered = await fetchOpenAICompatibleModels({
|
|
2228
|
+
api: providerConfig.api,
|
|
2229
|
+
provider: providerConfig.provider as Provider,
|
|
2230
|
+
baseUrl,
|
|
2231
|
+
apiKey: bearerToken,
|
|
2222
2232
|
headers,
|
|
2223
|
-
signal: AbortSignal.timeout(250),
|
|
2233
|
+
signal: AbortSignal.timeout(providerConfig.provider === "sglang" ? 500 : 250),
|
|
2234
|
+
fetch: (input, init) => fetch(input, { ...init, redirect: "error" }),
|
|
2235
|
+
throwOnStatus: response => new Error(`HTTP ${response.status} from ${baseUrl}/models`),
|
|
2236
|
+
mapModel: (item, defaults) => ({
|
|
2237
|
+
...defaults,
|
|
2238
|
+
reasoning: isOmlx,
|
|
2239
|
+
...(isOmlx
|
|
2240
|
+
? {
|
|
2241
|
+
thinking: {
|
|
2242
|
+
mode: "effort" as const,
|
|
2243
|
+
minLevel: Effort.Low,
|
|
2244
|
+
maxLevel: Effort.High,
|
|
2245
|
+
defaultLevel: Effort.Medium,
|
|
2246
|
+
levels: [Effort.Low, Effort.Medium, Effort.High],
|
|
2247
|
+
},
|
|
2248
|
+
}
|
|
2249
|
+
: {}),
|
|
2250
|
+
contextWindow:
|
|
2251
|
+
parseDiscoveryLimit(item.max_model_len) ??
|
|
2252
|
+
parseDiscoveryLimit(item.context_length) ??
|
|
2253
|
+
parseDiscoveryLimit(item.context_window) ??
|
|
2254
|
+
parseDiscoveryLimit(item.max_context_length) ??
|
|
2255
|
+
UNK_CONTEXT_WINDOW,
|
|
2256
|
+
maxTokens:
|
|
2257
|
+
parseDiscoveryLimit(item.max_completion_tokens) ??
|
|
2258
|
+
parseDiscoveryLimit(item.max_tokens) ??
|
|
2259
|
+
parseDiscoveryLimit(item.max_output_tokens) ??
|
|
2260
|
+
UNK_MAX_TOKENS,
|
|
2261
|
+
headers,
|
|
2262
|
+
compat: {
|
|
2263
|
+
...(providerConfig.compat ?? {}),
|
|
2264
|
+
supportsStore: false,
|
|
2265
|
+
supportsDeveloperRole: false,
|
|
2266
|
+
supportsReasoningEffort: isOmlx,
|
|
2267
|
+
...(isOmlx
|
|
2268
|
+
? {
|
|
2269
|
+
thinkingFormat: "qwen-chat-template" as const,
|
|
2270
|
+
reasoningContentField: "reasoning_content" as const,
|
|
2271
|
+
}
|
|
2272
|
+
: {}),
|
|
2273
|
+
},
|
|
2274
|
+
}),
|
|
2224
2275
|
});
|
|
2225
|
-
if (
|
|
2226
|
-
throw new Error(`HTTP ${response.status} from ${modelsUrl}`);
|
|
2227
|
-
}
|
|
2228
|
-
const payload = (await response.json()) as { data?: Array<{ id: string }> };
|
|
2229
|
-
const models = payload.data ?? [];
|
|
2230
|
-
const discovered: Model<Api>[] = [];
|
|
2231
|
-
for (const item of models) {
|
|
2232
|
-
const id = item.id;
|
|
2233
|
-
if (!id) continue;
|
|
2234
|
-
discovered.push(
|
|
2235
|
-
enrichModelThinking({
|
|
2236
|
-
id,
|
|
2237
|
-
name: id,
|
|
2238
|
-
api: providerConfig.api,
|
|
2239
|
-
provider: providerConfig.provider,
|
|
2240
|
-
baseUrl,
|
|
2241
|
-
reasoning: false,
|
|
2242
|
-
input: ["text"],
|
|
2243
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
2244
|
-
contextWindow: 128000,
|
|
2245
|
-
maxTokens: 8192,
|
|
2246
|
-
headers,
|
|
2247
|
-
compat: {
|
|
2248
|
-
supportsStore: false,
|
|
2249
|
-
supportsDeveloperRole: false,
|
|
2250
|
-
supportsReasoningEffort: false,
|
|
2251
|
-
},
|
|
2252
|
-
}),
|
|
2253
|
-
);
|
|
2254
|
-
}
|
|
2276
|
+
if (discovered === null) throw new Error(`Invalid OpenAI-compatible model catalog from ${baseUrl}`);
|
|
2255
2277
|
return this.#applyProviderModelOverrides(providerConfig.provider, discovered);
|
|
2256
2278
|
}
|
|
2257
2279
|
|
|
@@ -184,7 +184,7 @@ export const ModelOverrideSchema = z
|
|
|
184
184
|
export type ModelOverride = z.infer<typeof ModelOverrideSchema>;
|
|
185
185
|
|
|
186
186
|
export const ProviderDiscoverySchema = z.object({
|
|
187
|
-
type: z.enum(["ollama", "llama.cpp", "lm-studio", "openai-models-list"]),
|
|
187
|
+
type: z.enum(["ollama", "llama.cpp", "lm-studio", "omlx", "sglang", "openai-models-list"]),
|
|
188
188
|
});
|
|
189
189
|
|
|
190
190
|
const LocalOpenAICompatSchema = z
|