@gajae-code/coding-agent 0.7.6 → 0.7.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 +26 -1
- package/README.md +8 -0
- package/dist/types/cli/local-provider-smoke.d.ts +59 -0
- package/dist/types/cli/notify-cli.d.ts +1 -1
- package/dist/types/cli/update-cli.d.ts +24 -1
- package/dist/types/commands/local-provider.d.ts +34 -0
- package/dist/types/config/model-profiles.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +8 -3
- package/dist/types/config/models-config-schema.d.ts +5 -5
- package/dist/types/config/settings-schema.d.ts +123 -9
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/coordinator-mcp/server.d.ts +8 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +2 -2
- package/dist/types/gjc-runtime/state-renderer.d.ts +5 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/tmux-common.d.ts +12 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +37 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +80 -0
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +4 -1
- package/dist/types/modes/components/status-line/types.d.ts +5 -7
- package/dist/types/modes/components/status-line.d.ts +4 -3
- package/dist/types/modes/controllers/input-controller.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -0
- package/dist/types/modes/tmux-scroll.d.ts +18 -0
- package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
- package/dist/types/notifications/telegram-daemon-cli.d.ts +13 -2
- package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
- package/dist/types/tools/computer-gc.d.ts +23 -0
- package/dist/types/tools/cron.d.ts +36 -61
- package/dist/types/tools/index.d.ts +0 -1
- package/dist/types/tools/resource-gc.d.ts +54 -0
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +11 -4
- package/package.json +7 -7
- package/src/cli/args.ts +0 -1
- package/src/cli/fast-help.ts +0 -1
- package/src/cli/local-provider-smoke.ts +632 -0
- package/src/cli/notify-cli.ts +4 -2
- package/src/cli/update-cli.ts +155 -13
- package/src/cli/web-search-cli.ts +5 -0
- package/src/cli.ts +18 -0
- package/src/commands/launch.ts +46 -1
- package/src/commands/local-provider.ts +62 -0
- package/src/commands/notify.ts +1 -1
- package/src/config/model-profiles.ts +3 -4
- package/src/config/model-registry.ts +58 -7
- package/src/config/models-config-schema.ts +10 -1
- package/src/config/settings-schema.ts +127 -10
- package/src/config/settings.ts +11 -0
- package/src/coordinator-mcp/server.ts +276 -34
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/gjc-runtime/launch-tmux.ts +34 -8
- package/src/gjc-runtime/launch-worktree.ts +16 -2
- package/src/gjc-runtime/state-renderer.ts +13 -0
- package/src/gjc-runtime/team-runtime.ts +21 -6
- package/src/gjc-runtime/tmux-common.ts +16 -0
- package/src/gjc-runtime/tmux-sessions.ts +13 -9
- package/src/gjc-runtime/ultragoal-guard.ts +167 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +221 -1
- package/src/goals/tools/goal-tool.ts +14 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/components/agent-dashboard.ts +2 -1
- package/src/modes/components/custom-model-preset-wizard.ts +30 -189
- package/src/modes/components/model-selector.ts +182 -5
- package/src/modes/components/session-observer-overlay.ts +3 -3
- package/src/modes/components/settings-selector.ts +491 -5
- package/src/modes/components/status-line/segments.ts +7 -18
- package/src/modes/components/status-line/types.ts +6 -2
- package/src/modes/components/status-line.ts +69 -35
- package/src/modes/controllers/event-controller.ts +98 -4
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/input-controller.ts +15 -0
- package/src/modes/controllers/selector-controller.ts +18 -4
- package/src/modes/prompt-action-autocomplete.ts +8 -0
- package/src/modes/tmux-scroll.ts +92 -0
- package/src/modes/utils/terminal-bell.ts +48 -0
- package/src/notifications/telegram-daemon-cli.ts +112 -9
- package/src/notifications/telegram-daemon.ts +4 -0
- package/src/prompts/system/system-prompt.md +2 -2
- package/src/prompts/tools/cron.md +5 -3
- package/src/prompts/tools/read.md +1 -1
- package/src/sdk.ts +5 -0
- package/src/session/agent-session.ts +118 -5
- package/src/slash-commands/builtin-registry.ts +106 -13
- package/src/tools/browser/tab-supervisor.ts +86 -2
- package/src/tools/computer-gc.ts +66 -0
- package/src/tools/computer.ts +2 -0
- package/src/tools/cron.ts +75 -112
- package/src/tools/index.ts +2 -8
- package/src/tools/read.ts +25 -55
- package/src/tools/renderers.ts +0 -2
- package/src/tools/resource-gc.ts +291 -0
- package/src/tools/ultragoal-ask-guard.ts +7 -1
- package/src/web/search/index.ts +1 -0
- package/src/web/search/providers/utils.ts +22 -5
- package/vendor/insane-search/MANIFEST.json +3 -1
- package/vendor/insane-search/engine/__init__.py +14 -0
- package/vendor/insane-search/engine/content_safety.py +151 -0
- package/vendor/insane-search/engine/fetch_chain.py +32 -0
- package/vendor/insane-search/engine/tests/test_u8.py +216 -0
- package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
- package/dist/types/tools/inspect-image.d.ts +0 -31
- package/src/prompts/tools/inspect-image-system.md +0 -20
- package/src/prompts/tools/inspect-image.md +0 -32
- package/src/tools/inspect-image-renderer.ts +0 -103
- package/src/tools/inspect-image.ts +0 -172
package/src/cli/update-cli.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { theme } from "../modes/theme/theme";
|
|
|
14
14
|
|
|
15
15
|
const RELEASE_REPO = "Yeachan-Heo/gajae-code";
|
|
16
16
|
const PACKAGE = "@gajae-code/coding-agent";
|
|
17
|
+
const NPM_WRAPPER_PACKAGE = "gajae-code";
|
|
18
|
+
const NPM_MANAGED_PACKAGES = [NPM_WRAPPER_PACKAGE, PACKAGE] as const;
|
|
17
19
|
|
|
18
20
|
interface ReleaseInfo {
|
|
19
21
|
tag: string;
|
|
@@ -27,6 +29,23 @@ export interface InstalledVersionVerification {
|
|
|
27
29
|
path?: string;
|
|
28
30
|
smokeTestFailed?: boolean;
|
|
29
31
|
smokeTestOutput?: string;
|
|
32
|
+
cleanupWarning?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PackageManagerUpdateResult {
|
|
36
|
+
exitCode: number | null;
|
|
37
|
+
text: () => string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type PackageManagerUpdateRunner = (expectedVersion: string) => Promise<PackageManagerUpdateResult>;
|
|
41
|
+
|
|
42
|
+
export interface PackageManagerUpdateOptions {
|
|
43
|
+
managerName: string;
|
|
44
|
+
expectedVersion: string;
|
|
45
|
+
runInstall: PackageManagerUpdateRunner;
|
|
46
|
+
verifyInstalledRuntime: (expectedVersion: string) => Promise<InstalledVersionVerification>;
|
|
47
|
+
printVerificationResult?: (expectedVersion: string) => Promise<void>;
|
|
48
|
+
printRecoveredVerification?: (expectedVersion: string) => void;
|
|
30
49
|
}
|
|
31
50
|
|
|
32
51
|
/** Paths and verifier used while replacing a downloaded binary update. */
|
|
@@ -102,23 +121,76 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
|
|
|
102
121
|
return isPathInDirectoryLexical(resolvedFile, dirReal);
|
|
103
122
|
}
|
|
104
123
|
|
|
105
|
-
type
|
|
124
|
+
type PackageManagerTarget = { manager: "npm"; packageName: string };
|
|
125
|
+
type UpdateTarget = { method: "bun" } | { method: "npm"; packageName: string } | { method: "binary"; path: string };
|
|
126
|
+
|
|
127
|
+
type PathPlatform = NodeJS.Platform;
|
|
128
|
+
type PackageExists = (packageName: string, packageRoot: string) => boolean;
|
|
129
|
+
|
|
130
|
+
function pathApiForPlatform(platform: PathPlatform): typeof path.posix | typeof path.win32 {
|
|
131
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function defaultPackageExists(_packageName: string, packageRoot: string): boolean {
|
|
135
|
+
return fs.existsSync(path.join(packageRoot, "package.json"));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function npmPackageRootForBinPath(binPath: string, packageName: string, platform: PathPlatform): string {
|
|
139
|
+
const pathApi = pathApiForPlatform(platform);
|
|
140
|
+
const segments = packageName.split("/");
|
|
141
|
+
return pathApi.join(pathApi.dirname(binPath), "node_modules", ...segments);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveNpmManagedTarget(
|
|
145
|
+
ompPath: string,
|
|
146
|
+
platform: PathPlatform = process.platform,
|
|
147
|
+
packageExists: PackageExists = defaultPackageExists,
|
|
148
|
+
): PackageManagerTarget | undefined {
|
|
149
|
+
if (platform !== "win32") return undefined;
|
|
150
|
+
const pathApi = pathApiForPlatform(platform);
|
|
151
|
+
const extension = pathApi.extname(ompPath).toLowerCase();
|
|
152
|
+
if (extension !== ".cmd" && extension !== ".ps1") return undefined;
|
|
153
|
+
const basename = pathApi.basename(ompPath, extension).toLowerCase();
|
|
154
|
+
if (basename !== APP_NAME.toLowerCase()) return undefined;
|
|
106
155
|
|
|
107
|
-
|
|
156
|
+
for (const packageName of NPM_MANAGED_PACKAGES) {
|
|
157
|
+
const packageRoot = npmPackageRootForBinPath(ompPath, packageName, platform);
|
|
158
|
+
if (packageExists(packageName, packageRoot)) return { manager: "npm", packageName };
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): "bun" | "npm" | "binary" {
|
|
164
|
+
if (resolveNpmManagedTarget(ompPath)) return "npm";
|
|
108
165
|
if (!bunBinDir) return "binary";
|
|
109
166
|
return isPathInDirectory(ompPath, bunBinDir) ? "bun" : "binary";
|
|
110
167
|
}
|
|
111
168
|
|
|
112
|
-
export function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "binary" {
|
|
169
|
+
export function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "npm" | "binary" {
|
|
113
170
|
return resolveUpdateMethod(ompPath, bunBinDir);
|
|
114
171
|
}
|
|
172
|
+
|
|
173
|
+
export function resolveNpmManagedTargetForTest(
|
|
174
|
+
ompPath: string,
|
|
175
|
+
platform: PathPlatform,
|
|
176
|
+
packageExists: PackageExists,
|
|
177
|
+
): PackageManagerTarget | undefined {
|
|
178
|
+
return resolveNpmManagedTarget(ompPath, platform, packageExists);
|
|
179
|
+
}
|
|
115
180
|
async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
116
181
|
const bunBinDir = await getBunGlobalBinDir();
|
|
117
182
|
const ompPath = resolveGjcPath();
|
|
118
183
|
|
|
119
184
|
if (ompPath) {
|
|
185
|
+
const npmTarget = resolveNpmManagedTarget(ompPath);
|
|
186
|
+
if (npmTarget) return { method: "npm", packageName: npmTarget.packageName };
|
|
120
187
|
const method = resolveUpdateMethod(ompPath, bunBinDir);
|
|
121
188
|
if (method === "bun") return { method };
|
|
189
|
+
if (method === "npm") {
|
|
190
|
+
throw new Error(
|
|
191
|
+
formatUnsupportedTargetMessage(`Could not resolve npm package root for ${APP_NAME} shim ${ompPath}`),
|
|
192
|
+
);
|
|
193
|
+
}
|
|
122
194
|
return { method, path: ompPath };
|
|
123
195
|
}
|
|
124
196
|
|
|
@@ -262,6 +334,11 @@ function printVerifiedVersion(expectedVersion: string): void {
|
|
|
262
334
|
console.log(chalk.green(`\n${theme.status.success} Updated to ${expectedVersion}`));
|
|
263
335
|
}
|
|
264
336
|
|
|
337
|
+
function printSuccessfulVerification(expectedVersion: string): void {
|
|
338
|
+
printVerifiedVersion(expectedVersion);
|
|
339
|
+
printRestartGuidance();
|
|
340
|
+
}
|
|
341
|
+
|
|
265
342
|
function formatBinaryInstallInstruction(platform: NodeJS.Platform = process.platform): string {
|
|
266
343
|
if (platform === "win32") {
|
|
267
344
|
return `For a supported binary install, reinstall with PowerShell: irm https://raw.githubusercontent.com/${RELEASE_REPO}/main/scripts/install.ps1 | iex`;
|
|
@@ -351,8 +428,7 @@ export function formatVerificationFailureForTest(
|
|
|
351
428
|
async function printVerification(expectedVersion: string): Promise<void> {
|
|
352
429
|
const result = await verifyInstalledRuntime(expectedVersion);
|
|
353
430
|
if (result.ok) {
|
|
354
|
-
|
|
355
|
-
printRestartGuidance();
|
|
431
|
+
printSuccessfulVerification(expectedVersion);
|
|
356
432
|
return;
|
|
357
433
|
}
|
|
358
434
|
console.log(chalk.yellow(`\nWarning: ${formatVerificationFailure(result, expectedVersion)}`));
|
|
@@ -367,6 +443,19 @@ async function unlinkIfExists(filePath: string): Promise<void> {
|
|
|
367
443
|
}
|
|
368
444
|
}
|
|
369
445
|
|
|
446
|
+
function formatBackupCleanupWarning(backupPath: string, err: unknown): string {
|
|
447
|
+
return `Installed update, but could not remove backup file ${backupPath}: ${err}. You can delete it manually after closing shells or antivirus processes that may still hold it.`;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function cleanupVerifiedBackup(backupPath: string): Promise<string | undefined> {
|
|
451
|
+
try {
|
|
452
|
+
await unlinkIfExists(backupPath);
|
|
453
|
+
return undefined;
|
|
454
|
+
} catch (err) {
|
|
455
|
+
return formatBackupCleanupWarning(backupPath, err);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
370
459
|
/**
|
|
371
460
|
* Atomically replace the installed binary and roll back if version verification fails.
|
|
372
461
|
*/
|
|
@@ -386,8 +475,8 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
|
|
|
386
475
|
}
|
|
387
476
|
|
|
388
477
|
backupReady = false;
|
|
389
|
-
await
|
|
390
|
-
return verification;
|
|
478
|
+
const cleanupWarning = await cleanupVerifiedBackup(options.backupPath);
|
|
479
|
+
return cleanupWarning ? { ...verification, cleanupWarning } : verification;
|
|
391
480
|
} catch (err) {
|
|
392
481
|
if (backupReady) {
|
|
393
482
|
await unlinkIfExists(options.targetPath);
|
|
@@ -398,17 +487,67 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
|
|
|
398
487
|
}
|
|
399
488
|
}
|
|
400
489
|
|
|
490
|
+
function formatPackageManagerInstallFailure(
|
|
491
|
+
managerName: string,
|
|
492
|
+
result: PackageManagerUpdateResult,
|
|
493
|
+
verification: InstalledVersionVerification,
|
|
494
|
+
expectedVersion: string,
|
|
495
|
+
): string {
|
|
496
|
+
const output = normalizeVerificationOutput(result.text());
|
|
497
|
+
const outputSuffix = output ? `: ${output}` : "";
|
|
498
|
+
return `${managerName} install failed with exit code ${result.exitCode ?? "unknown"}${outputSuffix}. ${formatVerificationFailure(verification, expectedVersion)}`;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export async function runPackageManagerUpdateForTest(
|
|
502
|
+
options: PackageManagerUpdateOptions,
|
|
503
|
+
): Promise<InstalledVersionVerification> {
|
|
504
|
+
return updateViaPackageManager(options);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
async function updateViaPackageManager(options: PackageManagerUpdateOptions): Promise<InstalledVersionVerification> {
|
|
508
|
+
const result = await options.runInstall(options.expectedVersion);
|
|
509
|
+
if (result.exitCode === 0) {
|
|
510
|
+
await (options.printVerificationResult ?? printVerification)(options.expectedVersion);
|
|
511
|
+
return await options.verifyInstalledRuntime(options.expectedVersion);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const verification = await options.verifyInstalledRuntime(options.expectedVersion);
|
|
515
|
+
if (verification.ok) {
|
|
516
|
+
console.warn(
|
|
517
|
+
chalk.yellow(
|
|
518
|
+
`${options.managerName} exited with ${result.exitCode ?? "unknown"}, but ${APP_NAME} now verifies as ${options.expectedVersion}. Treating the update as installed.`,
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
(options.printRecoveredVerification ?? printSuccessfulVerification)(options.expectedVersion);
|
|
522
|
+
return verification;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
throw new Error(
|
|
526
|
+
formatPackageManagerInstallFailure(options.managerName, result, verification, options.expectedVersion),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
401
530
|
/**
|
|
402
531
|
* Update via bun package manager.
|
|
403
532
|
*/
|
|
404
533
|
async function updateViaBun(expectedVersion: string): Promise<void> {
|
|
405
534
|
console.log(chalk.dim("Updating via bun..."));
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
535
|
+
await updateViaPackageManager({
|
|
536
|
+
managerName: "bun",
|
|
537
|
+
expectedVersion,
|
|
538
|
+
runInstall: async version => await $`bun install -g ${PACKAGE}@${version}`.nothrow(),
|
|
539
|
+
verifyInstalledRuntime,
|
|
540
|
+
});
|
|
541
|
+
}
|
|
410
542
|
|
|
411
|
-
|
|
543
|
+
async function updateViaNpm(packageName: string, expectedVersion: string): Promise<void> {
|
|
544
|
+
console.log(chalk.dim(`Updating npm-managed install via npm (${packageName})...`));
|
|
545
|
+
await updateViaPackageManager({
|
|
546
|
+
managerName: "npm",
|
|
547
|
+
expectedVersion,
|
|
548
|
+
runInstall: async version => await $`npm install -g ${packageName}@${version}`.nothrow(),
|
|
549
|
+
verifyInstalledRuntime,
|
|
550
|
+
});
|
|
412
551
|
}
|
|
413
552
|
|
|
414
553
|
/**
|
|
@@ -430,7 +569,7 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
|
|
|
430
569
|
await pipeline(response.body, fileStream);
|
|
431
570
|
|
|
432
571
|
console.log(chalk.dim("Installing update..."));
|
|
433
|
-
await replaceBinaryForUpdate({
|
|
572
|
+
const verification = await replaceBinaryForUpdate({
|
|
434
573
|
targetPath,
|
|
435
574
|
tempPath,
|
|
436
575
|
backupPath,
|
|
@@ -438,6 +577,7 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
|
|
|
438
577
|
verifyInstalledVersion: verifyInstalledRuntime,
|
|
439
578
|
});
|
|
440
579
|
printVerifiedVersion(expectedVersion);
|
|
580
|
+
if (verification.cleanupWarning) console.warn(chalk.yellow(verification.cleanupWarning));
|
|
441
581
|
printRestartGuidance();
|
|
442
582
|
}
|
|
443
583
|
|
|
@@ -479,6 +619,8 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
|
|
|
479
619
|
const target = await resolveUpdateTarget();
|
|
480
620
|
if (target.method === "bun") {
|
|
481
621
|
await updateViaBun(release.version);
|
|
622
|
+
} else if (target.method === "npm") {
|
|
623
|
+
await updateViaNpm(target.packageName, release.version);
|
|
482
624
|
} else {
|
|
483
625
|
await updateViaBinaryAt(target.path, release.version);
|
|
484
626
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type SearchQueryParams,
|
|
16
16
|
} from "../web/search/index";
|
|
17
17
|
import { SEARCH_PROVIDER_ORDER, setPreferredSearchProvider, setSearchFallbackProviders } from "../web/search/provider";
|
|
18
|
+
import { setSearchHardTimeoutMs } from "../web/search/providers/utils";
|
|
18
19
|
import { renderSearchResult } from "../web/search/render";
|
|
19
20
|
import type { SearchProviderId } from "../web/search/types";
|
|
20
21
|
|
|
@@ -179,6 +180,10 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
|
|
|
179
180
|
configuredFallback.filter(value => typeof value === "string" && isConfigurableSearchProviderId(value)),
|
|
180
181
|
);
|
|
181
182
|
}
|
|
183
|
+
const configuredTimeout = settings.get("web_search.timeout");
|
|
184
|
+
if (typeof configuredTimeout === "number" && Number.isFinite(configuredTimeout) && configuredTimeout > 0) {
|
|
185
|
+
setSearchHardTimeoutMs(configuredTimeout * 1000);
|
|
186
|
+
}
|
|
182
187
|
|
|
183
188
|
const params: SearchQueryParams = {
|
|
184
189
|
query: cmd.query,
|
package/src/cli.ts
CHANGED
|
@@ -38,6 +38,7 @@ export const commands: CommandEntry[] = [
|
|
|
38
38
|
{ name: "notify", load: () => import("./commands/notify").then(m => m.default) },
|
|
39
39
|
{ name: "daemon", load: () => import("./commands/daemon").then(m => m.default) },
|
|
40
40
|
{ name: "web-search", aliases: ["q"], load: () => import("./commands/web-search").then(m => m.default) },
|
|
41
|
+
{ name: "local-provider", load: () => import("./commands/local-provider").then(m => m.default) },
|
|
41
42
|
{ name: "mcp-serve", load: () => import("./commands/mcp-serve").then(m => m.default) },
|
|
42
43
|
{ name: "mcp", load: () => import("./commands/mcp").then(m => m.default) },
|
|
43
44
|
{
|
|
@@ -79,6 +80,19 @@ async function installRuntimeGlobals(): Promise<void> {
|
|
|
79
80
|
delete process.env.MallocStackLoggingNoCompact;
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
function isNotifyDaemonInternalFastPath(argv: string[]): boolean {
|
|
84
|
+
return argv[0] === "notify" && argv[1] === "daemon-internal";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function runNotifyDaemonInternalFastPath(argv: string[]): Promise<void> {
|
|
88
|
+
const { parseNotifyArgs, runNotifyCommand } = await import("./cli/notify-cli");
|
|
89
|
+
const cmd = parseNotifyArgs(argv);
|
|
90
|
+
if (cmd?.action !== "daemon-internal") {
|
|
91
|
+
throw new Error("invalid notify daemon-internal fast path");
|
|
92
|
+
}
|
|
93
|
+
await runNotifyCommand(cmd);
|
|
94
|
+
}
|
|
95
|
+
|
|
82
96
|
function hasRootFastFlag(argv: string[], flags: readonly string[]): boolean {
|
|
83
97
|
for (const arg of argv) {
|
|
84
98
|
if (isSubcommand(arg)) return false;
|
|
@@ -208,6 +222,10 @@ async function runSmokeTest(): Promise<void> {
|
|
|
208
222
|
|
|
209
223
|
/** Run the CLI with the given argv (no `process.argv` prefix). */
|
|
210
224
|
export async function runCli(argv: string[]): Promise<void> {
|
|
225
|
+
if (isNotifyDaemonInternalFastPath(argv)) {
|
|
226
|
+
await runNotifyDaemonInternalFastPath(argv);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
211
229
|
if (argv[0] === "--smoke-test") {
|
|
212
230
|
await runSmokeTest();
|
|
213
231
|
return;
|
package/src/commands/launch.ts
CHANGED
|
@@ -2,15 +2,54 @@
|
|
|
2
2
|
* Root command for the coding agent CLI.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import * as fs from "node:fs/promises";
|
|
6
|
+
import * as path from "node:path";
|
|
5
7
|
import { THINKING_EFFORTS } from "@gajae-code/ai";
|
|
6
8
|
import { APP_NAME, setProjectDir } from "@gajae-code/utils";
|
|
7
9
|
import { Args, Command, Flags } from "@gajae-code/utils/cli";
|
|
8
10
|
import { parseArgs } from "../cli/args";
|
|
9
11
|
import { launchDefaultTmuxIfNeeded } from "../gjc-runtime/launch-tmux";
|
|
10
12
|
import { prepareLaunchWorktree } from "../gjc-runtime/launch-worktree";
|
|
13
|
+
import {
|
|
14
|
+
GJC_COORDINATOR_SESSION_ID_ENV,
|
|
15
|
+
GJC_COORDINATOR_SESSION_STATE_FILE_ENV,
|
|
16
|
+
} from "../gjc-runtime/session-state-sidecar";
|
|
11
17
|
import { runRootCommand } from "../main";
|
|
12
18
|
import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
|
|
13
19
|
|
|
20
|
+
async function persistCoordinatorLaunchFailure(error: unknown, cwd: string): Promise<void> {
|
|
21
|
+
const stateFile = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
22
|
+
if (!stateFile) return;
|
|
23
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
+
const code = message.split(":", 1)[0] || "launch_failed";
|
|
25
|
+
const now = new Date().toISOString();
|
|
26
|
+
const payload = {
|
|
27
|
+
schema_version: 1,
|
|
28
|
+
session_id: process.env[GJC_COORDINATOR_SESSION_ID_ENV]?.trim() || null,
|
|
29
|
+
state: "errored",
|
|
30
|
+
ready_for_input: false,
|
|
31
|
+
updated_at: now,
|
|
32
|
+
current_turn_id: null,
|
|
33
|
+
last_turn_id: null,
|
|
34
|
+
live: false,
|
|
35
|
+
reason: code,
|
|
36
|
+
source: "agent_session_event",
|
|
37
|
+
event: "launch_error",
|
|
38
|
+
cwd,
|
|
39
|
+
session_file: null,
|
|
40
|
+
final_response: {
|
|
41
|
+
text: message,
|
|
42
|
+
format: "markdown",
|
|
43
|
+
source: "launch_error",
|
|
44
|
+
artifact_path: null,
|
|
45
|
+
truncated: false,
|
|
46
|
+
},
|
|
47
|
+
error: { code, message, recoverable: true },
|
|
48
|
+
};
|
|
49
|
+
await fs.mkdir(path.dirname(stateFile), { recursive: true });
|
|
50
|
+
await Bun.write(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
|
|
51
|
+
}
|
|
52
|
+
|
|
14
53
|
export default class Index extends Command {
|
|
15
54
|
static description = "Red-claw AI coding assistant";
|
|
16
55
|
static hidden = true;
|
|
@@ -157,7 +196,13 @@ export default class Index extends Command {
|
|
|
157
196
|
return;
|
|
158
197
|
}
|
|
159
198
|
|
|
160
|
-
|
|
199
|
+
let launch: ReturnType<typeof prepareLaunchWorktree>;
|
|
200
|
+
try {
|
|
201
|
+
launch = prepareLaunchWorktree(process.cwd(), args);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
await persistCoordinatorLaunchFailure(error, process.cwd());
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
161
206
|
if (launch.worktree.enabled) {
|
|
162
207
|
process.chdir(launch.cwd);
|
|
163
208
|
setProjectDir(launch.cwd);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test configured local OpenAI-compatible providers.
|
|
3
|
+
*/
|
|
4
|
+
import { Args, Command, Flags } from "@gajae-code/utils/cli";
|
|
5
|
+
import {
|
|
6
|
+
runLocalProviderDiscoverCommand,
|
|
7
|
+
runLocalProviderSmokeCommand,
|
|
8
|
+
runLocalProviderStatusCommand,
|
|
9
|
+
} from "../cli/local-provider-smoke";
|
|
10
|
+
|
|
11
|
+
export const LOCAL_PROVIDER_ACTIONS = ["status", "diagnose", "discover", "models", "smoke"] as const;
|
|
12
|
+
export const LOCAL_PROVIDER_DEFAULT_ACTION = "status";
|
|
13
|
+
|
|
14
|
+
export default class LocalProvider extends Command {
|
|
15
|
+
static description = "Diagnose configured local OpenAI-compatible providers";
|
|
16
|
+
|
|
17
|
+
static args = {
|
|
18
|
+
action: Args.string({ description: "Action", required: false, options: LOCAL_PROVIDER_ACTIONS }),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
static flags = {
|
|
22
|
+
model: Flags.string({ description: "Model id to use for smoke (otherwise uses the first /models id)" }),
|
|
23
|
+
"models-path": Flags.string({ description: "Override models config path" }),
|
|
24
|
+
"timeout-ms": Flags.integer({ description: "Request timeout in milliseconds" }),
|
|
25
|
+
smoke: Flags.boolean({ description: "Run optional streaming chat smoke during status/diagnose" }),
|
|
26
|
+
json: Flags.boolean({ description: "Output JSON" }),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
async run(): Promise<void> {
|
|
30
|
+
const { args, flags } = await this.parse(LocalProvider);
|
|
31
|
+
const action = args.action ?? LOCAL_PROVIDER_DEFAULT_ACTION;
|
|
32
|
+
if (action === "status" || action === "diagnose") {
|
|
33
|
+
await runLocalProviderStatusCommand({
|
|
34
|
+
model: flags.model,
|
|
35
|
+
modelsPath: flags["models-path"],
|
|
36
|
+
timeoutMs: flags["timeout-ms"],
|
|
37
|
+
smoke: flags.smoke,
|
|
38
|
+
json: flags.json,
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (action === "discover" || action === "models") {
|
|
43
|
+
await runLocalProviderDiscoverCommand({
|
|
44
|
+
modelsPath: flags["models-path"],
|
|
45
|
+
timeoutMs: flags["timeout-ms"],
|
|
46
|
+
json: flags.json,
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (action === "smoke") {
|
|
51
|
+
await runLocalProviderSmokeCommand({
|
|
52
|
+
model: flags.model,
|
|
53
|
+
modelsPath: flags["models-path"],
|
|
54
|
+
timeoutMs: flags["timeout-ms"],
|
|
55
|
+
json: flags.json,
|
|
56
|
+
});
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
process.stderr.write(`Unsupported local-provider action: ${action}\n`);
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/src/commands/notify.ts
CHANGED
|
@@ -23,8 +23,8 @@ export interface ModelProfileDefinition {
|
|
|
23
23
|
|
|
24
24
|
export interface ResolvedProfileBinding {
|
|
25
25
|
defaultSelector?: string;
|
|
26
|
-
modelRoles:
|
|
27
|
-
agentModelOverrides: Partial<Record<Exclude<ModelProfileRole, "default"
|
|
26
|
+
modelRoles: Record<string, string>;
|
|
27
|
+
agentModelOverrides: Partial<Record<Exclude<ModelProfileRole, "default">, string>>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
function parseModelSelectorProvider(selector: string): string | undefined {
|
|
@@ -383,9 +383,8 @@ export function mergeModelProfiles(userProfiles?: ModelsConfig["profiles"]): Map
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
export function resolveProfileBindings(definition: ModelProfileDefinition): ResolvedProfileBinding {
|
|
386
|
-
const { default: defaultSelector,
|
|
386
|
+
const { default: defaultSelector, executor, architect, planner, critic } = definition.modelMapping;
|
|
387
387
|
const modelRoles: ResolvedProfileBinding["modelRoles"] = {};
|
|
388
|
-
if (vision !== undefined) modelRoles.vision = vision;
|
|
389
388
|
const agentModelOverrides: ResolvedProfileBinding["agentModelOverrides"] = {};
|
|
390
389
|
if (executor !== undefined) agentModelOverrides.executor = executor;
|
|
391
390
|
if (architect !== undefined) agentModelOverrides.architect = architect;
|
|
@@ -70,7 +70,7 @@ export function isAuthenticated(apiKey: string | undefined | null): apiKey is st
|
|
|
70
70
|
return Boolean(apiKey) && apiKey !== kNoAuth;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export type ModelRole = "default"
|
|
73
|
+
export type ModelRole = "default";
|
|
74
74
|
|
|
75
75
|
export interface ModelRoleInfo {
|
|
76
76
|
tag?: string;
|
|
@@ -80,12 +80,11 @@ export interface ModelRoleInfo {
|
|
|
80
80
|
|
|
81
81
|
export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
|
|
82
82
|
default: { tag: "DEFAULT", name: "Default", color: "success" },
|
|
83
|
-
vision: { tag: "VISION", name: "Vision", color: "accent" },
|
|
84
83
|
};
|
|
85
84
|
|
|
86
|
-
export const MODEL_ROLE_IDS: ModelRole[] = ["default"
|
|
85
|
+
export const MODEL_ROLE_IDS: ModelRole[] = ["default"];
|
|
87
86
|
|
|
88
|
-
export type GjcModelAssignmentTargetId = "default" | "
|
|
87
|
+
export type GjcModelAssignmentTargetId = "default" | "executor" | "architect" | "planner" | "critic";
|
|
89
88
|
|
|
90
89
|
export interface GjcModelAssignmentTargetInfo extends ModelRoleInfo {
|
|
91
90
|
id: GjcModelAssignmentTargetId;
|
|
@@ -94,7 +93,6 @@ export interface GjcModelAssignmentTargetInfo extends ModelRoleInfo {
|
|
|
94
93
|
|
|
95
94
|
export const GJC_MODEL_ASSIGNMENT_TARGET_IDS: GjcModelAssignmentTargetId[] = [
|
|
96
95
|
"default",
|
|
97
|
-
"vision",
|
|
98
96
|
"executor",
|
|
99
97
|
"architect",
|
|
100
98
|
"planner",
|
|
@@ -103,7 +101,6 @@ export const GJC_MODEL_ASSIGNMENT_TARGET_IDS: GjcModelAssignmentTargetId[] = [
|
|
|
103
101
|
|
|
104
102
|
export const GJC_MODEL_ASSIGNMENT_TARGETS: Record<GjcModelAssignmentTargetId, GjcModelAssignmentTargetInfo> = {
|
|
105
103
|
default: { id: "default", tag: "DEFAULT", name: "Default", color: "success", settingsPath: "modelRoles" },
|
|
106
|
-
vision: { id: "vision", tag: "VISION", name: "Vision", color: "accent", settingsPath: "modelRoles" },
|
|
107
104
|
executor: {
|
|
108
105
|
id: "executor",
|
|
109
106
|
tag: "EXECUTOR",
|
|
@@ -246,6 +243,7 @@ interface ProviderValidationConfig {
|
|
|
246
243
|
requestTransform?: ModelRequestTransform;
|
|
247
244
|
disableStrictTools?: boolean;
|
|
248
245
|
cacheRetention?: CacheRetention;
|
|
246
|
+
openaiCompat?: { baseUrl: string; apiKey?: string; apiKeyEnv?: string };
|
|
249
247
|
modelOverrides?: Record<string, unknown>;
|
|
250
248
|
models: ProviderValidationModel[];
|
|
251
249
|
}
|
|
@@ -274,11 +272,12 @@ function validateProviderConfiguration(
|
|
|
274
272
|
!config.disableStrictTools &&
|
|
275
273
|
!config.requestTransform &&
|
|
276
274
|
!config.cacheRetention &&
|
|
275
|
+
!config.openaiCompat &&
|
|
277
276
|
!hasModelOverrides &&
|
|
278
277
|
!config.discovery
|
|
279
278
|
) {
|
|
280
279
|
throw new Error(
|
|
281
|
-
`Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "cacheRetention", "disableStrictTools", "modelOverrides", "discovery", or "models"`,
|
|
280
|
+
`Provider ${providerName}: must specify "baseUrl", "headers", "apiKey", "compat", "requestTransform", "cacheRetention", "disableStrictTools", "modelOverrides", "discovery", "openaiCompat", or "models"`,
|
|
282
281
|
);
|
|
283
282
|
}
|
|
284
283
|
}
|
|
@@ -392,6 +391,7 @@ export const ModelsConfigFile = new ConfigFile<ModelsConfig>("models", ModelsCon
|
|
|
392
391
|
requestTransform: providerConfig.requestTransform,
|
|
393
392
|
disableStrictTools: providerConfig.disableStrictTools,
|
|
394
393
|
cacheRetention: providerConfig.cacheRetention,
|
|
394
|
+
openaiCompat: providerConfig.openaiCompat,
|
|
395
395
|
modelOverrides: providerConfig.modelOverrides,
|
|
396
396
|
models: (providerConfig.models ?? []) as ProviderValidationModel[],
|
|
397
397
|
},
|
|
@@ -439,6 +439,18 @@ function resolveProviderBaseUrlFromEnv(provider: string): string | undefined {
|
|
|
439
439
|
return $pickenv(...getProviderBaseUrlEnvKeys(provider));
|
|
440
440
|
}
|
|
441
441
|
|
|
442
|
+
function normalizeLocalOpenAICompatBaseUrl(baseUrl: string): string {
|
|
443
|
+
try {
|
|
444
|
+
const parsed = new URL(baseUrl);
|
|
445
|
+
const trimmedPath = parsed.pathname.replace(/\/+$/g, "");
|
|
446
|
+
parsed.pathname = trimmedPath.endsWith("/v1") ? trimmedPath || "/v1" : `${trimmedPath}/v1`;
|
|
447
|
+
return `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
|
|
448
|
+
} catch {
|
|
449
|
+
const trimmed = baseUrl.replace(/\/+$/g, "");
|
|
450
|
+
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
442
454
|
/**
|
|
443
455
|
* Merge a freshly discovered model with the matching bundled/configured entry
|
|
444
456
|
* (or a runtime provider override when no bundled entry exists).
|
|
@@ -1412,6 +1424,45 @@ export class ModelRegistry {
|
|
|
1412
1424
|
for (const [providerName, providerConfig] of providerEntries) {
|
|
1413
1425
|
if (providerConfig.webSearch) this.#providerWebSearchModes.set(providerName, providerConfig.webSearch);
|
|
1414
1426
|
const providerApiKeyConfig = providerConfig.apiKey ?? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv);
|
|
1427
|
+
const localOpenAICompat = providerConfig.openaiCompat;
|
|
1428
|
+
const localOpenAICompatApiKeyConfig = localOpenAICompat
|
|
1429
|
+
? (localOpenAICompat.apiKey ?? resolveApiKeyEnvConfig(localOpenAICompat.apiKeyEnv))
|
|
1430
|
+
: undefined;
|
|
1431
|
+
if (localOpenAICompat) {
|
|
1432
|
+
const localOpenAICompatBaseUrl = normalizeLocalOpenAICompatBaseUrl(localOpenAICompat.baseUrl);
|
|
1433
|
+
const localCompatResolvedKey = localOpenAICompat.apiKeyEnv
|
|
1434
|
+
? resolveApiKeyEnvConfig(localOpenAICompat.apiKeyEnv)
|
|
1435
|
+
: localOpenAICompat.apiKey
|
|
1436
|
+
? resolveApiKeyConfig(localOpenAICompat.apiKey)
|
|
1437
|
+
: undefined;
|
|
1438
|
+
overrides.set(providerName, {
|
|
1439
|
+
baseUrl: localOpenAICompatBaseUrl,
|
|
1440
|
+
apiKey: localOpenAICompatApiKeyConfig,
|
|
1441
|
+
compat: {
|
|
1442
|
+
supportsStore: false,
|
|
1443
|
+
supportsDeveloperRole: false,
|
|
1444
|
+
supportsReasoningEffort: false,
|
|
1445
|
+
},
|
|
1446
|
+
});
|
|
1447
|
+
discoverableProviders.push({
|
|
1448
|
+
provider: providerName,
|
|
1449
|
+
api: "openai-completions",
|
|
1450
|
+
baseUrl: localOpenAICompatBaseUrl,
|
|
1451
|
+
compat: {
|
|
1452
|
+
supportsStore: false,
|
|
1453
|
+
supportsDeveloperRole: false,
|
|
1454
|
+
supportsReasoningEffort: false,
|
|
1455
|
+
},
|
|
1456
|
+
discovery: { type: "openai-models-list" },
|
|
1457
|
+
optional: false,
|
|
1458
|
+
});
|
|
1459
|
+
if (localCompatResolvedKey) {
|
|
1460
|
+
this.#customProviderApiKeys.set(providerName, localCompatResolvedKey);
|
|
1461
|
+
this.authStorage.setConfigApiKey(providerName, localCompatResolvedKey);
|
|
1462
|
+
} else {
|
|
1463
|
+
keylessProviders.add(providerName);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1415
1466
|
// Always set overrides when baseUrl/headers/apiKey/authHeader/compat/disableStrictTools/transport are present
|
|
1416
1467
|
if (
|
|
1417
1468
|
providerConfig.baseUrl ||
|
|
@@ -81,7 +81,7 @@ const ModelBindingsSchema = z.object({
|
|
|
81
81
|
modelRoles: z.record(z.string(), z.string().min(1)).optional(),
|
|
82
82
|
agentModelOverrides: z.record(z.string(), z.string().min(1)).optional(),
|
|
83
83
|
});
|
|
84
|
-
export const ProfileRoleSchema = z.enum(["default", "
|
|
84
|
+
export const ProfileRoleSchema = z.enum(["default", "executor", "architect", "planner", "critic"]);
|
|
85
85
|
|
|
86
86
|
function isValidProfileModelSelector(value: string): boolean {
|
|
87
87
|
if (value.includes(",")) return false;
|
|
@@ -193,6 +193,14 @@ export const ProviderDiscoverySchema = z.object({
|
|
|
193
193
|
type: z.enum(["ollama", "llama.cpp", "lm-studio", "openai-models-list"]),
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
const LocalOpenAICompatSchema = z
|
|
197
|
+
.object({
|
|
198
|
+
baseUrl: z.string().min(1),
|
|
199
|
+
apiKey: z.string().min(1).optional(),
|
|
200
|
+
apiKeyEnv: z.string().min(1).optional(),
|
|
201
|
+
})
|
|
202
|
+
.strict();
|
|
203
|
+
|
|
196
204
|
export const ProviderAuthSchema = z.enum(["apiKey", "none", "oauth"]);
|
|
197
205
|
|
|
198
206
|
export type ProviderAuthMode = z.infer<typeof ProviderAuthSchema>;
|
|
@@ -237,6 +245,7 @@ const ProviderConfigSchema = z
|
|
|
237
245
|
*/
|
|
238
246
|
transport: z.literal("pi-native").optional(),
|
|
239
247
|
cacheRetention: CacheRetentionSchema.optional(),
|
|
248
|
+
openaiCompat: LocalOpenAICompatSchema.optional(),
|
|
240
249
|
})
|
|
241
250
|
.strict();
|
|
242
251
|
|