@gajae-code/coding-agent 0.5.1 → 0.5.2
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 +17 -0
- package/README.md +1 -1
- package/dist/types/cli/setup-cli.d.ts +8 -1
- package/dist/types/commands/setup.d.ts +7 -0
- package/dist/types/config/file-lock.d.ts +24 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +5 -0
- package/dist/types/config/settings-schema.d.ts +62 -0
- package/dist/types/gjc-runtime/state-writer.d.ts +64 -2
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +29 -0
- package/dist/types/modes/components/provider-onboarding-selector.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +56 -1
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +10 -0
- package/dist/types/modes/theme/defaults/index.d.ts +302 -0
- package/dist/types/modes/theme/theme.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/session/history-storage.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +10 -1
- package/dist/types/setup/credential-import.d.ts +79 -0
- package/dist/types/task/executor.d.ts +1 -0
- package/dist/types/task/render.d.ts +1 -1
- package/dist/types/tools/subagent-render.d.ts +7 -1
- package/dist/types/tools/subagent.d.ts +21 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +4 -4
- package/dist/types/web/search/provider.d.ts +16 -20
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/openai-compatible.d.ts +9 -0
- package/dist/types/web/search/types.d.ts +14 -2
- package/package.json +7 -7
- package/scripts/build-binary.ts +7 -0
- package/src/cli/args.ts +2 -0
- package/src/cli/fast-help.ts +2 -0
- package/src/cli/setup-cli.ts +138 -3
- package/src/commands/setup.ts +5 -1
- package/src/commands/ultragoal.ts +3 -1
- package/src/config/file-lock-gc.ts +14 -2
- package/src/config/file-lock.ts +54 -12
- package/src/config/model-profile-activation.ts +15 -3
- package/src/config/model-profiles.ts +15 -15
- package/src/config/model-registry.ts +21 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +62 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +30 -8
- package/src/gjc-runtime/deep-interview-recorder.ts +40 -0
- package/src/gjc-runtime/launch-tmux.ts +3 -4
- package/src/gjc-runtime/ralplan-runtime.ts +174 -12
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +254 -7
- package/src/gjc-runtime/tmux-gc.ts +2 -1
- package/src/gjc-runtime/ultragoal-guard.ts +155 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +1227 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +44 -0
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/harness-control-plane/owner.ts +3 -2
- package/src/harness-control-plane/rpc-adapter.ts +1 -1
- package/src/hooks/skill-state.ts +121 -2
- package/src/internal-urls/docs-index.generated.ts +13 -9
- package/src/lsp/defaults.json +1 -0
- package/src/main.ts +14 -4
- package/src/modes/acp/acp-agent.ts +4 -2
- package/src/modes/bridge/bridge-mode.ts +2 -1
- package/src/modes/components/history-search.ts +5 -2
- package/src/modes/components/model-selector.ts +26 -0
- package/src/modes/components/provider-onboarding-selector.ts +6 -1
- package/src/modes/controllers/selector-controller.ts +80 -1
- package/src/modes/interactive-mode.ts +11 -1
- package/src/modes/rpc/rpc-mode.ts +132 -18
- package/src/modes/shared/agent-wire/command-dispatch.ts +5 -2
- package/src/modes/shared/agent-wire/host-tool-bridge.ts +3 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/modes/theme/defaults/claude-code.json +100 -0
- package/src/modes/theme/defaults/codex.json +100 -0
- package/src/modes/theme/defaults/index.ts +6 -0
- package/src/modes/theme/defaults/opencode.json +102 -0
- package/src/modes/theme/theme.ts +2 -2
- package/src/modes/types.ts +1 -1
- package/src/prompts/agents/executor.md +5 -2
- package/src/sdk.ts +12 -1
- package/src/session/agent-session.ts +22 -11
- package/src/session/history-storage.ts +32 -11
- package/src/session/session-manager.ts +70 -18
- package/src/setup/credential-import.ts +429 -0
- package/src/skill-state/deep-interview-mutation-guard.ts +2 -1
- package/src/task/executor.ts +7 -1
- package/src/task/render.ts +18 -7
- package/src/tools/ask.ts +4 -2
- package/src/tools/cron.ts +1 -1
- package/src/tools/subagent-render.ts +119 -29
- package/src/tools/subagent.ts +147 -7
- package/src/tools/ultragoal-ask-guard.ts +39 -0
- package/src/web/search/index.ts +25 -25
- package/src/web/search/provider.ts +178 -87
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/openai-compatible.ts +151 -0
- package/src/web/search/types.ts +47 -22
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@gajae-code/coding-agent",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.2",
|
|
5
5
|
"description": "Gajae Code CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://gaebal-gajae.dev",
|
|
7
7
|
"author": "Yeachan-Heo",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"@agentclientprotocol/sdk": "0.21.0",
|
|
52
52
|
"@babel/parser": "^7.29.3",
|
|
53
53
|
"@mozilla/readability": "^0.6.0",
|
|
54
|
-
"@gajae-code/stats": "0.5.
|
|
55
|
-
"@gajae-code/agent-core": "0.5.
|
|
56
|
-
"@gajae-code/ai": "0.5.
|
|
57
|
-
"@gajae-code/natives": "0.5.
|
|
58
|
-
"@gajae-code/tui": "0.5.
|
|
59
|
-
"@gajae-code/utils": "0.5.
|
|
54
|
+
"@gajae-code/stats": "0.5.2",
|
|
55
|
+
"@gajae-code/agent-core": "0.5.2",
|
|
56
|
+
"@gajae-code/ai": "0.5.2",
|
|
57
|
+
"@gajae-code/natives": "0.5.2",
|
|
58
|
+
"@gajae-code/tui": "0.5.2",
|
|
59
|
+
"@gajae-code/utils": "0.5.2",
|
|
60
60
|
"@puppeteer/browsers": "^2.13.0",
|
|
61
61
|
"@types/turndown": "5.0.6",
|
|
62
62
|
"@xterm/headless": "^6.0.0",
|
package/scripts/build-binary.ts
CHANGED
|
@@ -5,6 +5,12 @@ import * as path from "node:path";
|
|
|
5
5
|
const packageDir = path.join(import.meta.dir, "..");
|
|
6
6
|
const outputPath = path.join(packageDir, "dist", "gjc");
|
|
7
7
|
const nativeDir = path.join(packageDir, "..", "natives", "native");
|
|
8
|
+
// Lazy native tokenizer entrypoint. `agent-core/compaction` loads this from
|
|
9
|
+
// the explicit native entrypoint instead of a package-name dynamic require of
|
|
10
|
+
// `@gajae-code/natives`, because those fail inside Bun standalone `$bunfs`.
|
|
11
|
+
// Listing the module here makes the absolute target path exist in the compiled
|
|
12
|
+
// bunfs.
|
|
13
|
+
const nativeTokenizerEntrypoint = "../natives/native/index.js";
|
|
8
14
|
|
|
9
15
|
function shouldAdhocSignDarwinBinary(): boolean {
|
|
10
16
|
return process.platform === "darwin";
|
|
@@ -66,6 +72,7 @@ async function main(): Promise<void> {
|
|
|
66
72
|
"../stats/src/sync-worker.ts",
|
|
67
73
|
"./src/tools/browser/tab-worker-entry.ts",
|
|
68
74
|
"./src/eval/js/worker-entry.ts",
|
|
75
|
+
nativeTokenizerEntrypoint,
|
|
69
76
|
"--outfile",
|
|
70
77
|
"dist/gjc",
|
|
71
78
|
],
|
package/src/cli/args.ts
CHANGED
|
@@ -269,6 +269,8 @@ export function getExtraHelpText(): string {
|
|
|
269
269
|
gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions
|
|
270
270
|
GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
|
|
271
271
|
GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
|
|
272
|
+
GJC_TMUX_PROFILE - Apply GJC tmux scroll/mouse/clipboard profile to --tmux sessions (set 0/off to skip)
|
|
273
|
+
GJC_MOUSE - Mouse-wheel scroll in --tmux sessions (set 0/off to let the host terminal scroll)
|
|
272
274
|
|
|
273
275
|
For complete environment variable reference, see:
|
|
274
276
|
${chalk.dim("docs/environment-variables.md")}
|
package/src/cli/fast-help.ts
CHANGED
|
@@ -54,6 +54,8 @@ export function getExtraHelpText(): string {
|
|
|
54
54
|
gjc session - List, inspect, create, remove, or attach tagged GJC-managed tmux sessions
|
|
55
55
|
GJC_LAUNCH_POLICY - Launch policy for --tmux startup: tmux or direct
|
|
56
56
|
GJC_TMUX_SESSION - Explicit tmux session name override for --tmux startup
|
|
57
|
+
GJC_TMUX_PROFILE - Apply GJC tmux scroll/mouse/clipboard profile to --tmux sessions (set 0/off to skip)
|
|
58
|
+
GJC_MOUSE - Mouse-wheel scroll in --tmux sessions (set 0/off to let the host terminal scroll)
|
|
57
59
|
|
|
58
60
|
For complete environment variable reference, see:
|
|
59
61
|
docs/environment-variables.md
|
package/src/cli/setup-cli.ts
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Handles `gjc setup [component]` to install the normal defaults or optional feature dependencies.
|
|
5
5
|
*/
|
|
6
|
+
|
|
6
7
|
import * as path from "node:path";
|
|
7
|
-
import {
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { SqliteAuthCredentialStore } from "@gajae-code/ai";
|
|
10
|
+
import { $which, APP_NAME, getAgentDbPath, getPythonEnvDir } from "@gajae-code/utils";
|
|
8
11
|
import { $ } from "bun";
|
|
9
12
|
import chalk from "chalk";
|
|
10
13
|
import { installDefaultGjcDefinitions } from "../defaults/gjc-defaults";
|
|
@@ -14,6 +17,7 @@ import {
|
|
|
14
17
|
readGjcManagedCodexHooksStatus,
|
|
15
18
|
} from "../hooks/codex-native-hooks-config";
|
|
16
19
|
import { theme } from "../modes/theme/theme";
|
|
20
|
+
import { discoverExternalCredentials, formatDiscoverySummary, importCredentials } from "../setup/credential-import";
|
|
17
21
|
import {
|
|
18
22
|
formatHermesSetupResult,
|
|
19
23
|
type HermesSetupFlags,
|
|
@@ -27,7 +31,7 @@ import {
|
|
|
27
31
|
parseProviderCompatibility,
|
|
28
32
|
} from "../setup/provider-onboarding";
|
|
29
33
|
|
|
30
|
-
export type SetupComponent = "defaults" | "hermes" | "hooks" | "provider" | "python" | "stt";
|
|
34
|
+
export type SetupComponent = "credentials" | "defaults" | "hermes" | "hooks" | "provider" | "python" | "stt";
|
|
31
35
|
|
|
32
36
|
export interface SetupCommandArgs {
|
|
33
37
|
component: SetupComponent;
|
|
@@ -57,10 +61,12 @@ export interface SetupCommandArgs {
|
|
|
57
61
|
gjcCommand?: string;
|
|
58
62
|
target?: string;
|
|
59
63
|
profileDir?: string;
|
|
64
|
+
yes?: boolean;
|
|
65
|
+
dryRun?: boolean;
|
|
60
66
|
};
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
const VALID_COMPONENTS: SetupComponent[] = ["defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
69
|
+
const VALID_COMPONENTS: SetupComponent[] = ["credentials", "defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
64
70
|
|
|
65
71
|
function hasProviderSetupFlags(flags: SetupCommandArgs["flags"]): boolean {
|
|
66
72
|
return (
|
|
@@ -113,6 +119,10 @@ export function parseSetupArgs(args: string[]): SetupCommandArgs | undefined {
|
|
|
113
119
|
flags.smoke = true;
|
|
114
120
|
} else if (arg === "--install") {
|
|
115
121
|
flags.install = true;
|
|
122
|
+
} else if (arg === "--yes" || arg === "-y") {
|
|
123
|
+
flags.yes = true;
|
|
124
|
+
} else if (arg === "--dry-run") {
|
|
125
|
+
flags.dryRun = true;
|
|
116
126
|
} else if (arg === "--root") {
|
|
117
127
|
flags.root = [...(flags.root ?? []), args[++i] ?? ""];
|
|
118
128
|
} else if (arg === "--repo") {
|
|
@@ -243,6 +253,9 @@ export async function runSetupCommand(cmd: SetupCommandArgs): Promise<void> {
|
|
|
243
253
|
case "stt":
|
|
244
254
|
await handleSttSetup(cmd.flags);
|
|
245
255
|
break;
|
|
256
|
+
case "credentials":
|
|
257
|
+
await handleCredentialsSetup(cmd.flags);
|
|
258
|
+
break;
|
|
246
259
|
}
|
|
247
260
|
}
|
|
248
261
|
|
|
@@ -472,6 +485,122 @@ async function handleSttSetup(flags: { json?: boolean; check?: boolean }): Promi
|
|
|
472
485
|
process.exit(1);
|
|
473
486
|
}
|
|
474
487
|
}
|
|
488
|
+
async function confirmImport(count: number): Promise<boolean> {
|
|
489
|
+
if (!process.stdin.isTTY) return false;
|
|
490
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
491
|
+
try {
|
|
492
|
+
const answer = (await rl.question(`Import ${count} credential(s) into ${getAgentDbPath()}? [y/N] `))
|
|
493
|
+
.trim()
|
|
494
|
+
.toLowerCase();
|
|
495
|
+
return answer === "y" || answer === "yes";
|
|
496
|
+
} finally {
|
|
497
|
+
rl.close();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Discover existing Claude Code / Codex CLI credentials and import them into the
|
|
503
|
+
* gjc credential store after a redacted preview + confirmation. Falls back to
|
|
504
|
+
* manual-setup guidance when nothing importable is found.
|
|
505
|
+
*/
|
|
506
|
+
async function handleCredentialsSetup(flags: { json?: boolean; yes?: boolean; dryRun?: boolean }): Promise<void> {
|
|
507
|
+
const result = await discoverExternalCredentials();
|
|
508
|
+
const redactedPlan = {
|
|
509
|
+
importable: result.importable.map(c => ({
|
|
510
|
+
provider: c.provider,
|
|
511
|
+
kind: c.kind,
|
|
512
|
+
source: c.source,
|
|
513
|
+
identity: c.identity,
|
|
514
|
+
expiresAt: c.expiresAt,
|
|
515
|
+
redactedToken: c.redactedToken,
|
|
516
|
+
})),
|
|
517
|
+
skipped: result.skipped,
|
|
518
|
+
environment: result.environment,
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
if (result.importable.length === 0) {
|
|
522
|
+
if (flags.json) {
|
|
523
|
+
process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
for (const line of formatDiscoverySummary(result)) process.stdout.write(` ${line}\n`);
|
|
527
|
+
process.stdout.write(
|
|
528
|
+
chalk.yellow(
|
|
529
|
+
`\nNo importable Claude/Codex credentials found. Continue with manual setup:\n` +
|
|
530
|
+
` ${APP_NAME} setup provider (add an API-compatible provider)\n` +
|
|
531
|
+
` ${APP_NAME} (then /login) (interactive OAuth/subscription login)\n`,
|
|
532
|
+
),
|
|
533
|
+
);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (!flags.json) {
|
|
538
|
+
process.stdout.write(chalk.bold("Discovered credentials (redacted):\n"));
|
|
539
|
+
for (const line of formatDiscoverySummary(result)) process.stdout.write(` ${line}\n`);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (flags.dryRun) {
|
|
543
|
+
if (flags.json) process.stdout.write(`${JSON.stringify({ ...redactedPlan, dryRun: true, imported: [] })}\n`);
|
|
544
|
+
else process.stdout.write(chalk.dim(`\nDry run — no credentials imported.\n`));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const confirmed = flags.yes || (await confirmImport(result.importable.length));
|
|
549
|
+
if (!confirmed) {
|
|
550
|
+
if (flags.json) {
|
|
551
|
+
process.stdout.write(`${JSON.stringify({ ...redactedPlan, imported: [] })}\n`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
process.stdout.write(chalk.dim(`\nImport cancelled. Re-run with --yes to import non-interactively.\n`));
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const store = await SqliteAuthCredentialStore.open(getAgentDbPath());
|
|
559
|
+
let summary: Awaited<ReturnType<typeof importCredentials>>;
|
|
560
|
+
try {
|
|
561
|
+
summary = await importCredentials(result.importable, (provider, credential) =>
|
|
562
|
+
store.upsertAuthCredentialForProvider(provider, credential),
|
|
563
|
+
);
|
|
564
|
+
} finally {
|
|
565
|
+
store.close();
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (flags.json) {
|
|
569
|
+
process.stdout.write(
|
|
570
|
+
`${JSON.stringify({
|
|
571
|
+
...redactedPlan,
|
|
572
|
+
imported: summary.imported.map(c => ({ provider: c.provider, kind: c.kind, source: c.source })),
|
|
573
|
+
failed: summary.failed.map(f => ({
|
|
574
|
+
provider: f.credential.provider,
|
|
575
|
+
source: f.credential.source,
|
|
576
|
+
error: f.error,
|
|
577
|
+
})),
|
|
578
|
+
})}\n`,
|
|
579
|
+
);
|
|
580
|
+
if (summary.failed.length > 0) process.exitCode = 1;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
for (const credential of summary.imported) {
|
|
585
|
+
process.stdout.write(
|
|
586
|
+
`${chalk.green(`${theme.status.success} imported`)} ${formatCredentialSummaryLine(credential)}\n`,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
for (const failure of summary.failed) {
|
|
590
|
+
process.stdout.write(
|
|
591
|
+
`${chalk.red(`${theme.status.error} failed`)} ${failure.credential.provider} (${failure.credential.source}): ${failure.error}\n`,
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
if (summary.failed.length > 0) {
|
|
595
|
+
process.exitCode = 1;
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
process.stdout.write(chalk.dim(`\nCredentials saved to ${getAgentDbPath()}\n`));
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function formatCredentialSummaryLine(credential: { provider: string; kind: string; source: string }): string {
|
|
602
|
+
return `${credential.provider} · ${credential.kind} (from ${credential.source})`;
|
|
603
|
+
}
|
|
475
604
|
|
|
476
605
|
/**
|
|
477
606
|
* Print setup command help.
|
|
@@ -489,6 +618,7 @@ ${chalk.bold("Components:")}
|
|
|
489
618
|
provider Optional: add a preset, OpenAI-compatible, or Anthropic-compatible API provider
|
|
490
619
|
python Optional: verify a Python 3 interpreter is reachable for code execution
|
|
491
620
|
stt Optional: install speech-to-text dependencies (openai-whisper, recording tools)
|
|
621
|
+
credentials Optional: import existing Claude Code / Codex CLI credentials
|
|
492
622
|
|
|
493
623
|
|
|
494
624
|
${chalk.bold("Provider example:")}
|
|
@@ -524,6 +654,8 @@ ${chalk.bold("Options:")}
|
|
|
524
654
|
--mutation Hermes MCP mutation classes: sessions,questions,reports,all
|
|
525
655
|
--target Hermes config file target for config-only install
|
|
526
656
|
--profile-dir Hermes profile directory for full setup install
|
|
657
|
+
--dry-run Preview discovered credentials without importing (credentials)
|
|
658
|
+
-y, --yes Import discovered credentials without an interactive prompt (credentials)
|
|
527
659
|
|
|
528
660
|
${chalk.bold("Examples:")}
|
|
529
661
|
${APP_NAME} setup Install bundled GJC default workflow skills
|
|
@@ -536,5 +668,8 @@ ${chalk.bold("Examples:")}
|
|
|
536
668
|
${APP_NAME} setup stt Install speech-to-text dependencies
|
|
537
669
|
${APP_NAME} setup stt --check Check if STT dependencies are available
|
|
538
670
|
${APP_NAME} setup python --check Check if Python execution is available
|
|
671
|
+
${APP_NAME} setup credentials Discover & import existing Claude/Codex credentials
|
|
672
|
+
${APP_NAME} setup credentials --dry-run Preview importable credentials (redacted)
|
|
673
|
+
${APP_NAME} setup credentials --yes Import without an interactive prompt
|
|
539
674
|
`);
|
|
540
675
|
}
|
package/src/commands/setup.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { Args, Command, Flags } from "@gajae-code/utils/cli";
|
|
|
5
5
|
import { runSetupCommand, type SetupCommandArgs, type SetupComponent } from "../cli/setup-cli";
|
|
6
6
|
import { initTheme } from "../modes/theme/theme";
|
|
7
7
|
|
|
8
|
-
const COMPONENTS: SetupComponent[] = ["defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
8
|
+
const COMPONENTS: SetupComponent[] = ["credentials", "defaults", "hermes", "hooks", "provider", "python", "stt"];
|
|
9
9
|
|
|
10
10
|
export default class Setup extends Command {
|
|
11
11
|
static description = "Install GJC defaults or optional feature dependencies";
|
|
@@ -47,6 +47,8 @@ export default class Setup extends Command {
|
|
|
47
47
|
"api-key-env": Flags.string({ description: "Read provider API key from this environment variable" }),
|
|
48
48
|
model: Flags.string({ description: "Model id to add (repeat or comma-separate)", multiple: true }),
|
|
49
49
|
"models-path": Flags.string({ description: "Override models config path" }),
|
|
50
|
+
yes: Flags.boolean({ char: "y", description: "Import discovered credentials without an interactive prompt" }),
|
|
51
|
+
"dry-run": Flags.boolean({ description: "Preview discovered credentials without importing" }),
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
async run(): Promise<void> {
|
|
@@ -79,6 +81,8 @@ export default class Setup extends Command {
|
|
|
79
81
|
gjcCommand: flags["gjc-command"],
|
|
80
82
|
target: flags.target,
|
|
81
83
|
profileDir: flags["profile-dir"],
|
|
84
|
+
yes: flags.yes,
|
|
85
|
+
dryRun: flags["dry-run"],
|
|
82
86
|
},
|
|
83
87
|
};
|
|
84
88
|
await initTheme();
|
|
@@ -16,12 +16,14 @@ export default class Ultragoal extends Command {
|
|
|
16
16
|
static delegateHelp = true;
|
|
17
17
|
|
|
18
18
|
async run(): Promise<void> {
|
|
19
|
+
const isReviewStart = this.argv.includes("review") && this.argv.includes("review-start");
|
|
19
20
|
const shouldActivateGoalMode = isUltragoalCreateGoalsInvocation(this.argv);
|
|
20
21
|
const result = await runNativeUltragoalCommand(this.argv);
|
|
21
22
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
22
23
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
23
24
|
process.exitCode = result.status;
|
|
24
|
-
if (result.status !== 0 || !shouldActivateGoalMode) return;
|
|
25
|
+
if (result.status !== 0 || (!shouldActivateGoalMode && !isReviewStart)) return;
|
|
26
|
+
if (isReviewStart && !result.createdReviewPlan && (result.reviewBlockerGoalIds?.length ?? 0) === 0) return;
|
|
25
27
|
|
|
26
28
|
const cwd = process.cwd();
|
|
27
29
|
const { objective, goalsPath } = await readUltragoalGjcObjective(cwd);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* GC adapter for config file-locks (`<file>.lock` dirs holding `{pid, timestamp}`).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { Stats } from "node:fs";
|
|
5
6
|
import * as fs from "node:fs/promises";
|
|
6
7
|
import * as path from "node:path";
|
|
7
8
|
import { getAgentDir, getConfigRootDir, isEnoent } from "@gajae-code/utils";
|
|
@@ -92,7 +93,7 @@ async function walkForLockDirs(
|
|
|
92
93
|
return;
|
|
93
94
|
}
|
|
94
95
|
|
|
95
|
-
let stat:
|
|
96
|
+
let stat: Stats;
|
|
96
97
|
try {
|
|
97
98
|
stat = await fs.lstat(dir);
|
|
98
99
|
} catch (error) {
|
|
@@ -171,8 +172,19 @@ export const fileLocksGcAdapter: GcStoreAdapter = {
|
|
|
171
172
|
return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
// Fail-closed owner-token guard (#606): we observed `info` (pid+timestamp)
|
|
176
|
+
// dead, but a fresh owner can reclaim a stale lock dir at this same path
|
|
177
|
+
// between the probe above and the unlink below. Pass the exact owner token
|
|
178
|
+
// so removal re-verifies the on-disk identity under the unlink and refuses
|
|
179
|
+
// to delete a recreated LIVE lock (TOCTOU).
|
|
174
180
|
try {
|
|
175
|
-
await removeFileLockDirForGc(lockDir);
|
|
181
|
+
const removal = await removeFileLockDirForGc(lockDir, info);
|
|
182
|
+
if (removal === "owner_changed") {
|
|
183
|
+
return { removed: false, skipped: "file_lock_owner_changed_before_delete" };
|
|
184
|
+
}
|
|
185
|
+
if (removal === "missing") {
|
|
186
|
+
return { removed: false, skipped: "lock_no_longer_dead_or_missing" };
|
|
187
|
+
}
|
|
176
188
|
return { removed: true };
|
|
177
189
|
} catch (error) {
|
|
178
190
|
return { removed: false, error: errorMessage(error) };
|
package/src/config/file-lock.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs/promises";
|
|
2
|
-
import { isEnoent } from "@gajae-code/utils";
|
|
2
|
+
import { isEnoent } from "@gajae-code/utils/fs-error";
|
|
3
3
|
|
|
4
4
|
export interface FileLockOptions {
|
|
5
5
|
staleMs?: number;
|
|
@@ -45,17 +45,57 @@ export async function readFileLockInfoForGc(lockDir: string): Promise<{ pid: num
|
|
|
45
45
|
return info;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
export
|
|
48
|
+
/** Owner identity stamped into a `<file>.lock/info` record. */
|
|
49
|
+
export interface FileLockOwnerToken {
|
|
50
|
+
pid: number;
|
|
51
|
+
timestamp: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Outcome of a guarded GC removal attempt (`removeFileLockDirForGc`). */
|
|
55
|
+
export type FileLockGcRemoval = "removed" | "owner_changed" | "missing";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @internal
|
|
59
|
+
* Fail-closed removal of a dead lock dir for GC. Re-reads the on-disk owner
|
|
60
|
+
* token as close to the unlink as possible and only deletes the dir when it
|
|
61
|
+
* STILL holds the exact `{pid, timestamp}` identity the caller observed dead.
|
|
62
|
+
*
|
|
63
|
+
* Closes the prune-time TOCTOU window (#606): between GC's dead re-read/probe
|
|
64
|
+
* and the unlink, a live process can reclaim a stale lock at the same path
|
|
65
|
+
* (`acquireLock` rms the stale dir, then re-`mkdir`s and rewrites `info` with a
|
|
66
|
+
* fresh pid+timestamp). Deleting by path alone would reap that LIVE lock. Any
|
|
67
|
+
* mismatch (`owner_changed`) or absent/unreadable info (`missing` — e.g. a
|
|
68
|
+
* fresh acquirer between `mkdir` and `writeLockInfo`) refuses the delete and
|
|
69
|
+
* leaves the dir intact. POSIX has no atomic compare-and-delete for a
|
|
70
|
+
* directory, so the residual read->unlink window cannot be fully eliminated,
|
|
71
|
+
* but the reclaim-after-stale scenario the issue describes is now guarded.
|
|
72
|
+
*/
|
|
73
|
+
export async function removeFileLockDirForGc(
|
|
74
|
+
lockDir: string,
|
|
75
|
+
expected: FileLockOwnerToken,
|
|
76
|
+
): Promise<FileLockGcRemoval> {
|
|
77
|
+
const current = await readLockInfo(lockDir);
|
|
78
|
+
if (!current) return "missing";
|
|
79
|
+
if (current.pid !== expected.pid || current.timestamp !== expected.timestamp) {
|
|
80
|
+
return "owner_changed";
|
|
81
|
+
}
|
|
50
82
|
await fs.rm(lockDir, { recursive: true, force: true });
|
|
83
|
+
return "removed";
|
|
51
84
|
}
|
|
52
85
|
|
|
53
|
-
|
|
86
|
+
type OwnerLiveness = "alive" | "dead" | "unknown";
|
|
87
|
+
|
|
88
|
+
function ownerLiveness(pid: number): OwnerLiveness {
|
|
89
|
+
if (!Number.isFinite(pid) || pid <= 0) return "unknown";
|
|
54
90
|
try {
|
|
55
91
|
process.kill(pid, 0);
|
|
56
|
-
return
|
|
57
|
-
} catch {
|
|
58
|
-
|
|
92
|
+
return "alive";
|
|
93
|
+
} catch (error) {
|
|
94
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
95
|
+
if (code === "ESRCH") return "dead";
|
|
96
|
+
// EPERM means the process exists but we may not signal it; treat as alive.
|
|
97
|
+
// Anything else is indeterminate.
|
|
98
|
+
return code === "EPERM" ? "alive" : "unknown";
|
|
59
99
|
}
|
|
60
100
|
}
|
|
61
101
|
|
|
@@ -63,11 +103,13 @@ async function isLockStale(lockPath: string, staleMs: number): Promise<boolean>
|
|
|
63
103
|
const info = await readLockInfo(lockPath);
|
|
64
104
|
if (!info) return true;
|
|
65
105
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return
|
|
106
|
+
// Never reap a live owner by elapsed time: a long legitimate critical section must
|
|
107
|
+
// not have its lock stolen (#652). Reclaim a dead owner immediately. Only when owner
|
|
108
|
+
// liveness is indeterminate do we fall back to the staleMs elapsed-time heuristic.
|
|
109
|
+
const liveness = ownerLiveness(info.pid);
|
|
110
|
+
if (liveness === "dead") return true;
|
|
111
|
+
if (liveness === "alive") return false;
|
|
112
|
+
return Date.now() - info.timestamp > staleMs;
|
|
71
113
|
}
|
|
72
114
|
|
|
73
115
|
async function tryAcquireLock(lockPath: string): Promise<boolean> {
|
|
@@ -11,6 +11,8 @@ import { type GjcModelAssignmentTargetId, isAuthenticated, type ModelRegistry }
|
|
|
11
11
|
import { resolveModelRoleValue } from "./model-resolver";
|
|
12
12
|
import type { Settings } from "./settings";
|
|
13
13
|
|
|
14
|
+
const LEGACY_MODEL_PROFILE_ALIASES: ReadonlyMap<string, string> = new Map([["codex-standard", "codex-medium"]]);
|
|
15
|
+
|
|
14
16
|
type ModelProfileActivationSession = Pick<AgentSession, "model" | "thinkingLevel" | "sessionId"> & {
|
|
15
17
|
setModelTemporary?: AgentSession["setModelTemporary"];
|
|
16
18
|
setActiveModelProfile?: (name: string | undefined) => void;
|
|
@@ -51,12 +53,22 @@ export function formatModelProfileCredentialError(profileName: string, providers
|
|
|
51
53
|
return `Model profile "${profileName}" requires credentials for: ${providers.join(", ")}. Run /login and configure the missing provider(s), then retry.`;
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
function resolveModelProfileName(profileName: string, profiles: ReadonlyMap<string, unknown>): string {
|
|
57
|
+
// A retired-name alias is fallback-only: never shadow a profile that actually
|
|
58
|
+
// exists under the requested name (e.g. a user-defined `codex-standard`).
|
|
59
|
+
if (profiles.has(profileName)) return profileName;
|
|
60
|
+
const replacement = LEGACY_MODEL_PROFILE_ALIASES.get(profileName);
|
|
61
|
+
return replacement && profiles.has(replacement) ? replacement : profileName;
|
|
62
|
+
}
|
|
63
|
+
|
|
54
64
|
export async function prepareModelProfileActivation(
|
|
55
65
|
options: PrepareModelProfileActivationOptions,
|
|
56
66
|
): Promise<PreparedModelProfileActivation> {
|
|
57
|
-
const
|
|
67
|
+
const profiles = options.modelRegistry.getModelProfiles();
|
|
68
|
+
const profileName = resolveModelProfileName(options.profileName, profiles);
|
|
69
|
+
const profile = profiles.get(profileName) ?? options.modelRegistry.getModelProfile(profileName);
|
|
58
70
|
if (!profile) {
|
|
59
|
-
const available = formatAvailableProfileNames(
|
|
71
|
+
const available = formatAvailableProfileNames(profiles);
|
|
60
72
|
throw new Error(`Unknown model profile "${options.profileName}". Available profiles: ${available}`);
|
|
61
73
|
}
|
|
62
74
|
|
|
@@ -101,7 +113,7 @@ export async function prepareModelProfileActivation(
|
|
|
101
113
|
}
|
|
102
114
|
|
|
103
115
|
return {
|
|
104
|
-
profileName
|
|
116
|
+
profileName,
|
|
105
117
|
session: options.session as PreparedModelProfileActivation["session"],
|
|
106
118
|
settings: options.settings as PreparedModelProfileActivation["settings"],
|
|
107
119
|
previousModel: options.session.model,
|
|
@@ -202,25 +202,25 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
202
202
|
architect: "cursor/composer-1.5:xhigh",
|
|
203
203
|
}),
|
|
204
204
|
profile("minimax-eco", ["minimax-code"], {
|
|
205
|
-
default: "minimax-code/minimax-
|
|
206
|
-
executor: "minimax-code/minimax-
|
|
207
|
-
planner: "minimax-code/minimax-
|
|
208
|
-
critic: "minimax-code/minimax-
|
|
209
|
-
architect: "minimax-code/minimax-
|
|
205
|
+
default: "minimax-code/minimax-m3:low",
|
|
206
|
+
executor: "minimax-code/minimax-m3:minimal",
|
|
207
|
+
planner: "minimax-code/minimax-m3:low",
|
|
208
|
+
critic: "minimax-code/minimax-m3:medium",
|
|
209
|
+
architect: "minimax-code/minimax-m3:high",
|
|
210
210
|
}),
|
|
211
211
|
profile("minimax-medium", ["minimax-code"], {
|
|
212
|
-
default: "minimax-code/minimax-
|
|
213
|
-
executor: "minimax-code/minimax-
|
|
214
|
-
planner: "minimax-code/minimax-
|
|
215
|
-
critic: "minimax-code/minimax-
|
|
216
|
-
architect: "minimax-code/minimax-
|
|
212
|
+
default: "minimax-code/minimax-m3:medium",
|
|
213
|
+
executor: "minimax-code/minimax-m3:low",
|
|
214
|
+
planner: "minimax-code/minimax-m3:medium",
|
|
215
|
+
critic: "minimax-code/minimax-m3:high",
|
|
216
|
+
architect: "minimax-code/minimax-m3:xhigh",
|
|
217
217
|
}),
|
|
218
218
|
profile("minimax-pro", ["minimax-code"], {
|
|
219
|
-
default: "minimax-code/minimax-
|
|
220
|
-
executor: "minimax-code/minimax-
|
|
221
|
-
planner: "minimax-code/minimax-
|
|
222
|
-
critic: "minimax-code/minimax-
|
|
223
|
-
architect: "minimax-code/minimax-
|
|
219
|
+
default: "minimax-code/minimax-m3:xhigh",
|
|
220
|
+
executor: "minimax-code/minimax-m3:medium",
|
|
221
|
+
planner: "minimax-code/minimax-m3:high",
|
|
222
|
+
critic: "minimax-code/minimax-m3:xhigh",
|
|
223
|
+
architect: "minimax-code/minimax-m3:xhigh",
|
|
224
224
|
}),
|
|
225
225
|
profile("opus-codex", ["anthropic", "openai-codex"], {
|
|
226
226
|
default: "anthropic/claude-opus-4-8:xhigh",
|
|
@@ -35,6 +35,7 @@ import { $pickenv, isRecord, logger } from "@gajae-code/utils";
|
|
|
35
35
|
import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
|
|
36
36
|
import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
|
|
37
37
|
import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
|
|
38
|
+
import type { ActiveSearchModelContext, WebSearchMode } from "../web/search/types";
|
|
38
39
|
import { type ConfigError, ConfigFile } from "./config-file";
|
|
39
40
|
import {
|
|
40
41
|
buildCanonicalModelIndex,
|
|
@@ -910,7 +911,7 @@ function finalizeCustomModel(model: CustomModelOverlay, options: CustomModelBuil
|
|
|
910
911
|
const output = resolvedModel.output ?? reference?.output;
|
|
911
912
|
return enrichModelThinking({
|
|
912
913
|
id: resolvedModel.id,
|
|
913
|
-
name: resolvedModel.name ?? (options.useDefaults ? resolvedModel.id : undefined),
|
|
914
|
+
name: resolvedModel.name ?? reference?.name ?? (options.useDefaults ? resolvedModel.id : undefined),
|
|
914
915
|
api: resolvedModel.api,
|
|
915
916
|
provider: resolvedModel.provider,
|
|
916
917
|
baseUrl: resolvedModel.baseUrl,
|
|
@@ -964,6 +965,7 @@ export class ModelRegistry {
|
|
|
964
965
|
#models: Model<Api>[] = [];
|
|
965
966
|
#canonicalIndex: CanonicalModelIndex = { records: [], byId: new Map(), bySelector: new Map() };
|
|
966
967
|
#customProviderApiKeys: Map<string, string> = new Map();
|
|
968
|
+
#providerWebSearchModes: Map<string, WebSearchMode> = new Map();
|
|
967
969
|
#keylessProviders: Set<string> = new Set();
|
|
968
970
|
#discoverableProviders: DiscoveryProviderConfig[] = [];
|
|
969
971
|
#customModelOverlays: CustomModelOverlay[] = [];
|
|
@@ -1073,6 +1075,7 @@ export class ModelRegistry {
|
|
|
1073
1075
|
}
|
|
1074
1076
|
this.#modelsConfigFile.invalidate();
|
|
1075
1077
|
this.#customProviderApiKeys.clear();
|
|
1078
|
+
this.#providerWebSearchModes.clear();
|
|
1076
1079
|
this.#keylessProviders.clear();
|
|
1077
1080
|
this.#discoverableProviders = [];
|
|
1078
1081
|
// Drop config-sourced apiKeys from AuthStorage before reload; entries
|
|
@@ -1390,6 +1393,7 @@ export class ModelRegistry {
|
|
|
1390
1393
|
const configuredProviders = new Set(Object.keys(value.providers ?? {}));
|
|
1391
1394
|
|
|
1392
1395
|
for (const [providerName, providerConfig] of providerEntries) {
|
|
1396
|
+
if (providerConfig.webSearch) this.#providerWebSearchModes.set(providerName, providerConfig.webSearch);
|
|
1393
1397
|
const providerApiKeyConfig = providerConfig.apiKey ?? resolveApiKeyEnvConfig(providerConfig.apiKeyEnv);
|
|
1394
1398
|
// Always set overrides when baseUrl/headers/apiKey/authHeader/compat/disableStrictTools/transport are present
|
|
1395
1399
|
if (
|
|
@@ -2441,6 +2445,22 @@ export class ModelRegistry {
|
|
|
2441
2445
|
);
|
|
2442
2446
|
}
|
|
2443
2447
|
|
|
2448
|
+
getProviderWebSearchMode(provider: string): WebSearchMode | undefined {
|
|
2449
|
+
return this.#providerWebSearchModes.get(provider);
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
getActiveSearchModelContext(model: Model<Api>): ActiveSearchModelContext {
|
|
2453
|
+
return {
|
|
2454
|
+
provider: model.provider,
|
|
2455
|
+
modelId: model.id,
|
|
2456
|
+
wireModelId: model.wireModelId,
|
|
2457
|
+
api: model.api,
|
|
2458
|
+
baseUrl: model.baseUrl,
|
|
2459
|
+
headers: model.headers,
|
|
2460
|
+
webSearch: this.getProviderWebSearchMode(model.provider),
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2444
2464
|
/**
|
|
2445
2465
|
* Get API key for a model.
|
|
2446
2466
|
*/
|
|
@@ -218,6 +218,7 @@ const ProviderConfigSchema = z
|
|
|
218
218
|
.optional(),
|
|
219
219
|
headers: z.record(z.string(), z.string()).optional(),
|
|
220
220
|
compat: OpenAICompatSchema.optional(),
|
|
221
|
+
webSearch: z.enum(["on", "off", "auto"]).optional(),
|
|
221
222
|
authHeader: z.boolean().optional(),
|
|
222
223
|
auth: ProviderAuthSchema.optional(),
|
|
223
224
|
discovery: ProviderDiscoverySchema.optional(),
|