@gajae-code/coding-agent 0.12.8 → 0.12.11
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 +36 -0
- package/dist/types/cli/notify-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +23 -0
- package/dist/types/discovery/agents-md.d.ts +11 -1
- package/dist/types/modes/types.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -1
- package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +23 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +20 -2
- package/dist/types/session/internal/native-publish-outcome.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +17 -0
- package/dist/types/session/session-storage.d.ts +1 -0
- package/dist/types/system-prompt.d.ts +12 -1
- package/dist/types/tools/composer-bash-policy.d.ts +9 -1
- package/dist/types/tools/computer.d.ts +35 -2
- package/dist/types/tools/todo-write.d.ts +2 -0
- package/package.json +8 -8
- package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
- package/src/cli/notify-cli.ts +12 -6
- package/src/commands/sdk.ts +54 -0
- package/src/config/model-profiles.ts +8 -0
- package/src/config/model-registry.ts +4 -1
- package/src/discovery/agents-md.ts +76 -19
- package/src/gjc-runtime/psmux-detect.ts +12 -16
- package/src/gjc-runtime/tmux-sessions.ts +8 -1
- package/src/internal-urls/docs-index.generated.ts +6 -5
- package/src/modes/components/extensions/inspector-panel.ts +9 -5
- package/src/modes/controllers/selector-controller.ts +61 -43
- package/src/modes/print-mode.ts +3 -0
- package/src/modes/types.ts +6 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/computer.md +6 -1
- package/src/sdk/bus/chat-daemon-control.ts +5 -3
- package/src/sdk/bus/daemon-paths.ts +2 -0
- package/src/sdk/bus/notification-service.ts +140 -7
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -3
- package/src/sdk/session.ts +24 -7
- package/src/session/agent-session.ts +176 -34
- package/src/session/internal/managed-session-storage.ts +280 -248
- package/src/session/internal/native-publish-outcome.ts +12 -2
- package/src/session/session-manager.ts +140 -6
- package/src/session/session-storage.ts +3 -0
- package/src/slash-commands/builtin-registry.ts +35 -11
- package/src/system-prompt.ts +28 -15
- package/src/tools/bash.ts +3 -1
- package/src/tools/composer-bash-policy.ts +21 -5
- package/src/tools/computer.ts +322 -45
- package/src/tools/todo-write.ts +29 -11
- package/src/utils/command-args.ts +19 -30
- package/src/utils/lang-from-path.ts +2 -4
package/src/commands/sdk.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { applyStartupModelProfiles, createSessionManager } from "../main";
|
|
|
9
9
|
import { initializeExtensions } from "../modes/runtime-init";
|
|
10
10
|
import { ACP_MCP_REQUEST_TIMEOUT_MS, ACP_MCP_STARTUP_HEADROOM_MS } from "../sdk/acp/mcp";
|
|
11
11
|
import { Broker } from "../sdk/broker/broker";
|
|
12
|
+
import { readBrokerDiscovery } from "../sdk/broker/discovery";
|
|
12
13
|
import { completeBrokerProcess } from "../sdk/broker/internal";
|
|
13
14
|
import {
|
|
14
15
|
type LifecycleTranscriptEvidence,
|
|
@@ -58,6 +59,54 @@ export async function lifecycleArgs(
|
|
|
58
59
|
};
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
/**
|
|
63
|
+
* How long a session host tolerates the complete absence of a live broker
|
|
64
|
+
* publication before treating itself as orphaned. Hosts intentionally survive
|
|
65
|
+
* broker restarts (a replacement broker republishes discovery within seconds),
|
|
66
|
+
* so this must comfortably exceed a restart window while still bounding the
|
|
67
|
+
* lifetime of hosts whose broker is gone for good — otherwise every crashed or
|
|
68
|
+
* torn-down broker leaks a detached multi-hundred-megabyte host forever.
|
|
69
|
+
*/
|
|
70
|
+
export const SESSION_HOST_BROKER_ABSENCE_GRACE_MS = 10 * 60_000;
|
|
71
|
+
const SESSION_HOST_BROKER_POLL_MS = 15_000;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolves only once no live broker publication has been observable in
|
|
75
|
+
* `agentDir` for the full grace window. A reappearing broker (including a
|
|
76
|
+
* replacement with a different pid) resets the window; an unreadable
|
|
77
|
+
* publication is not proof of orphanhood but accrues against the same bound.
|
|
78
|
+
*/
|
|
79
|
+
export async function watchSessionHostBrokerLiveness(deps: {
|
|
80
|
+
agentDir: string;
|
|
81
|
+
now?: () => number;
|
|
82
|
+
sleep?: (ms: number) => Promise<void>;
|
|
83
|
+
readDiscovery?: (agentDir: string) => Promise<unknown>;
|
|
84
|
+
graceMs?: number;
|
|
85
|
+
pollMs?: number;
|
|
86
|
+
}): Promise<void> {
|
|
87
|
+
const now = deps.now ?? Date.now;
|
|
88
|
+
const sleep = deps.sleep ?? (async ms => await Bun.sleep(ms));
|
|
89
|
+
const readDiscovery = deps.readDiscovery ?? readBrokerDiscovery;
|
|
90
|
+
const graceMs = deps.graceMs ?? SESSION_HOST_BROKER_ABSENCE_GRACE_MS;
|
|
91
|
+
const pollMs = deps.pollMs ?? SESSION_HOST_BROKER_POLL_MS;
|
|
92
|
+
let absentSince: number | null = null;
|
|
93
|
+
for (;;) {
|
|
94
|
+
let live: unknown = null;
|
|
95
|
+
try {
|
|
96
|
+
live = await readDiscovery(deps.agentDir);
|
|
97
|
+
} catch {
|
|
98
|
+
// Transient read failures are ambiguity, not proof of orphanhood.
|
|
99
|
+
}
|
|
100
|
+
if (live) {
|
|
101
|
+
absentSince = null;
|
|
102
|
+
} else {
|
|
103
|
+
absentSince ??= now();
|
|
104
|
+
if (now() - absentSince >= graceMs) return;
|
|
105
|
+
}
|
|
106
|
+
await sleep(pollMs);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
61
110
|
type LifecycleTranscriptSource = {
|
|
62
111
|
cwd: string;
|
|
63
112
|
path: string;
|
|
@@ -505,6 +554,11 @@ export async function runSessionHost(
|
|
|
505
554
|
}
|
|
506
555
|
process.once("SIGTERM", stop);
|
|
507
556
|
process.once("SIGINT", stop);
|
|
557
|
+
// A detached host whose broker is gone for good would otherwise live (and
|
|
558
|
+
// hold its session's memory) forever; reap it through the same graceful
|
|
559
|
+
// teardown a SIGTERM would take once the bounded absence grace elapses.
|
|
560
|
+
await watchSessionHostBrokerLiveness({ agentDir });
|
|
561
|
+
stop();
|
|
508
562
|
await new Promise<void>(() => {});
|
|
509
563
|
}
|
|
510
564
|
|
|
@@ -291,6 +291,13 @@ export const BUILTIN_MODEL_PROFILES: readonly ModelProfileDefinition[] = [
|
|
|
291
291
|
architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
|
|
292
292
|
critic: "alibaba-token-plan/glm-5.2:high",
|
|
293
293
|
}),
|
|
294
|
+
profile("alibaba-token-plan-pro", ["alibaba-token-plan"], {
|
|
295
|
+
default: "alibaba-token-plan/qwen3.8-max-preview:medium",
|
|
296
|
+
executor: "alibaba-token-plan/deepseek-v4-flash-0731:max",
|
|
297
|
+
planner: "alibaba-token-plan/glm-5.2:high",
|
|
298
|
+
architect: "alibaba-token-plan/qwen3.8-max-preview:xhigh",
|
|
299
|
+
critic: "alibaba-token-plan/glm-5.2:xhigh",
|
|
300
|
+
}),
|
|
294
301
|
profile("alibaba-token-plan-qwenmaxxing", ["alibaba-token-plan"], {
|
|
295
302
|
default: "alibaba-token-plan/qwen3.8-max-preview:medium",
|
|
296
303
|
executor: "alibaba-token-plan/qwen3.8-max-preview:low",
|
|
@@ -361,6 +368,7 @@ const PROFILE_PRESENTATION: Record<string, ModelProfilePresentation> = {
|
|
|
361
368
|
"minimax-medium": { displayName: "MiniMax Medium", providerGroup: "MINIMAX" },
|
|
362
369
|
"minimax-pro": { displayName: "MiniMax Pro", providerGroup: "MINIMAX" },
|
|
363
370
|
"alibaba-token-plan-balanced": { displayName: "Balanced", providerGroup: "ALIBABA TOKEN PLAN" },
|
|
371
|
+
"alibaba-token-plan-pro": { displayName: "Pro", providerGroup: "ALIBABA TOKEN PLAN" },
|
|
364
372
|
"alibaba-token-plan-qwenmaxxing": { displayName: "QwenMaxxing", providerGroup: "ALIBABA TOKEN PLAN" },
|
|
365
373
|
"opus-codex": { displayName: "Opus + Codex", providerGroup: "COMBOS" },
|
|
366
374
|
"codex-opencodego": { displayName: "Codex + OpenCodeGo", providerGroup: "COMBOS" },
|
|
@@ -331,7 +331,10 @@ function validateProviderConfiguration(
|
|
|
331
331
|
throw new Error(
|
|
332
332
|
mode === "runtime-register"
|
|
333
333
|
? `Provider ${providerName}: "apiKey" or "oauth" is required when defining models.`
|
|
334
|
-
: `Provider ${providerName}:
|
|
334
|
+
: `Provider ${providerName}: custom models need a credential source, but none is configured. ` +
|
|
335
|
+
`"auth" only selects the scheme ("auth: apiKey" does not supply a key). ` +
|
|
336
|
+
`Fix by adding "apiKeyEnv: <ENV_VAR>" (recommended) or "apiKey: <literal-key>", ` +
|
|
337
|
+
`or set "auth: none" if the endpoint is genuinely unauthenticated.`,
|
|
335
338
|
);
|
|
336
339
|
}
|
|
337
340
|
}
|
|
@@ -5,56 +5,113 @@
|
|
|
5
5
|
* This handles AGENTS.md files that live in project root (not in config directories
|
|
6
6
|
* like .OpenAI code backend/ or .gemini/, which are handled by their respective providers).
|
|
7
7
|
*/
|
|
8
|
+
import * as fs from "node:fs/promises";
|
|
8
9
|
import * as path from "node:path";
|
|
9
10
|
import { registerProvider } from "../capability";
|
|
10
11
|
import { type ContextFile, contextFileCapability } from "../capability/context-file";
|
|
11
|
-
import { readFile } from "../capability/fs";
|
|
12
12
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
13
13
|
import { calculateDepth, createSourceMeta } from "./helpers";
|
|
14
14
|
|
|
15
15
|
const PROVIDER_ID = "agents-md";
|
|
16
16
|
const DISPLAY_NAME = "AGENTS.md";
|
|
17
|
+
// Bound hostile ancestor walks and instruction payloads before they reach prompt assembly.
|
|
18
|
+
const MAX_ANCESTOR_DIRECTORIES = 32;
|
|
19
|
+
const MAX_FILE_BYTES = 64 * 1024;
|
|
20
|
+
const MAX_AGGREGATE_BYTES = 256 * 1024;
|
|
21
|
+
const DIRECTORY_LIMIT_WARNING = "AGENTS.md discovery stopped after scanning 32 ancestor directories.";
|
|
22
|
+
const FILE_LIMIT_WARNING = "Skipped one or more AGENTS.md files that exceed the 64 KiB limit.";
|
|
23
|
+
const AGGREGATE_LIMIT_WARNING = "Skipped one or more AGENTS.md files that exceed the 256 KiB aggregate limit.";
|
|
24
|
+
|
|
25
|
+
export type AgentsMdReader = (
|
|
26
|
+
filePath: string,
|
|
27
|
+
maxBytes: number,
|
|
28
|
+
) => Promise<{ content: string | null; byteLength: number; tooLarge: boolean }>;
|
|
29
|
+
|
|
30
|
+
async function readBoundedAgentsMdFile(
|
|
31
|
+
filePath: string,
|
|
32
|
+
maxBytes: number,
|
|
33
|
+
): Promise<{ content: string | null; byteLength: number; tooLarge: boolean }> {
|
|
34
|
+
try {
|
|
35
|
+
const file = await fs.open(filePath, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
|
|
36
|
+
try {
|
|
37
|
+
const stats = await file.stat();
|
|
38
|
+
if (!stats.isFile()) return { content: null, byteLength: 0, tooLarge: false };
|
|
39
|
+
const bytes = Buffer.alloc(maxBytes + 1);
|
|
40
|
+
let bytesRead = 0;
|
|
41
|
+
while (bytesRead < bytes.length) {
|
|
42
|
+
const result = await file.read(bytes, bytesRead, bytes.length - bytesRead, bytesRead);
|
|
43
|
+
if (result.bytesRead === 0) break;
|
|
44
|
+
bytesRead += result.bytesRead;
|
|
45
|
+
}
|
|
46
|
+
if (bytesRead > maxBytes) return { content: null, byteLength: bytesRead, tooLarge: true };
|
|
47
|
+
return {
|
|
48
|
+
content: new TextDecoder().decode(bytes.subarray(0, bytesRead)),
|
|
49
|
+
byteLength: bytesRead,
|
|
50
|
+
tooLarge: false,
|
|
51
|
+
};
|
|
52
|
+
} finally {
|
|
53
|
+
await file.close();
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
return { content: null, byteLength: 0, tooLarge: false };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
17
59
|
|
|
18
60
|
/**
|
|
19
61
|
* Load standalone AGENTS.md files.
|
|
20
62
|
*/
|
|
21
|
-
async function loadAgentsMd(
|
|
63
|
+
export async function loadAgentsMd(
|
|
64
|
+
ctx: LoadContext,
|
|
65
|
+
readCandidate: AgentsMdReader = readBoundedAgentsMdFile,
|
|
66
|
+
): Promise<LoadResult<ContextFile>> {
|
|
22
67
|
const items: ContextFile[] = [];
|
|
23
68
|
const warnings: string[] = [];
|
|
24
|
-
|
|
25
|
-
// Walk up from cwd looking for AGENTS.md files
|
|
26
69
|
let current = ctx.cwd;
|
|
70
|
+
let scannedDirectories = 0;
|
|
71
|
+
let aggregateBytes = 0;
|
|
72
|
+
let omittedOversizedFile = false;
|
|
73
|
+
let omittedAggregateFile = false;
|
|
27
74
|
|
|
28
75
|
while (true) {
|
|
76
|
+
scannedDirectories += 1;
|
|
29
77
|
const candidate = path.join(current, "AGENTS.md");
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
if (content !== null) {
|
|
33
|
-
const parent = path.dirname(candidate);
|
|
34
|
-
const baseName = parent.split(path.sep).pop() ?? "";
|
|
78
|
+
const parent = path.dirname(candidate);
|
|
79
|
+
const baseName = parent.split(path.sep).pop() ?? "";
|
|
35
80
|
|
|
36
|
-
|
|
81
|
+
if (!baseName.startsWith(".")) {
|
|
82
|
+
const remainingAggregateBytes = MAX_AGGREGATE_BYTES - aggregateBytes;
|
|
83
|
+
const allowedBytes = Math.min(MAX_FILE_BYTES, remainingAggregateBytes);
|
|
84
|
+
const result = await readCandidate(candidate, allowedBytes);
|
|
85
|
+
if (result.tooLarge) {
|
|
86
|
+
if (allowedBytes < MAX_FILE_BYTES) omittedAggregateFile = true;
|
|
87
|
+
else omittedOversizedFile = true;
|
|
88
|
+
} else if (result.content !== null) {
|
|
37
89
|
const fileDir = path.dirname(candidate);
|
|
38
|
-
const calculatedDepth = calculateDepth(ctx.cwd, fileDir, path.sep);
|
|
39
|
-
|
|
40
90
|
items.push({
|
|
41
91
|
path: candidate,
|
|
42
|
-
content,
|
|
92
|
+
content: result.content,
|
|
43
93
|
level: "project",
|
|
44
|
-
depth:
|
|
94
|
+
depth: calculateDepth(ctx.cwd, fileDir, path.sep),
|
|
45
95
|
_source: createSourceMeta(PROVIDER_ID, candidate, "project"),
|
|
46
96
|
});
|
|
97
|
+
aggregateBytes += result.byteLength;
|
|
47
98
|
}
|
|
48
99
|
}
|
|
49
100
|
|
|
50
|
-
|
|
101
|
+
const stopDirectory = ctx.repoRoot ?? ctx.home;
|
|
102
|
+
if (current === stopDirectory) break;
|
|
103
|
+
if (scannedDirectories === MAX_ANCESTOR_DIRECTORIES) {
|
|
104
|
+
warnings.push(DIRECTORY_LIMIT_WARNING);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
51
107
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
current = parent;
|
|
108
|
+
const parentDirectory = path.dirname(current);
|
|
109
|
+
if (parentDirectory === current) break;
|
|
110
|
+
current = parentDirectory;
|
|
56
111
|
}
|
|
57
112
|
|
|
113
|
+
if (omittedOversizedFile) warnings.push(FILE_LIMIT_WARNING);
|
|
114
|
+
if (omittedAggregateFile) warnings.push(AGGREGATE_LIMIT_WARNING);
|
|
58
115
|
return { items, warnings };
|
|
59
116
|
}
|
|
60
117
|
|
|
@@ -324,25 +324,21 @@ export function resolveGjcTmuxBinary(options: ResolveGjcTmuxBinaryOptions = {}):
|
|
|
324
324
|
return { command: explicit, isPsmux, viaExplicitOverride: true };
|
|
325
325
|
}
|
|
326
326
|
if (platform === "win32") {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
const executablePath = resolveBinaryPath(candidate);
|
|
327
|
+
for (const command of ["psmux", "pmux"] as const) {
|
|
328
|
+
const executablePath = resolveBinaryPath(command);
|
|
330
329
|
if (!executablePath) continue;
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (!identity)
|
|
335
|
-
throw new Error(`gjc_tmux_provider_ambiguous: Windows ${candidate} executable identity is unavailable`);
|
|
336
|
-
candidates.push({ command: candidate, identity });
|
|
330
|
+
if (!activeExecutableIdentityResolver(executablePath))
|
|
331
|
+
throw new Error(`gjc_tmux_provider_ambiguous: Windows ${command} executable identity is unavailable`);
|
|
332
|
+
return { command, isPsmux: true, viaExplicitOverride: false };
|
|
337
333
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return { command: candidates[0]!.command, isPsmux: true, viaExplicitOverride: false };
|
|
334
|
+
const tmuxPath = resolveBinaryPath("tmux");
|
|
335
|
+
if (tmuxPath) {
|
|
336
|
+
const isPsmux = outputMentionsPsmux(probeVersionOutput(tmuxPath, runner));
|
|
337
|
+
if (isPsmux && !activeExecutableIdentityResolver(tmuxPath))
|
|
338
|
+
throw new Error("gjc_tmux_provider_ambiguous: Windows tmux executable identity is unavailable");
|
|
339
|
+
return { command: "tmux", isPsmux, viaExplicitOverride: false };
|
|
345
340
|
}
|
|
341
|
+
return { command: "tmux", isPsmux: false, viaExplicitOverride: false };
|
|
346
342
|
}
|
|
347
343
|
const tmuxPath = resolveBinaryPath("tmux");
|
|
348
344
|
if (tmuxPath) {
|
|
@@ -228,7 +228,7 @@ function runTmux(
|
|
|
228
228
|
);
|
|
229
229
|
}
|
|
230
230
|
if (result.exitCode === 0) return result.stdout?.toString() ?? "";
|
|
231
|
-
throw new Error(result.stderr?.toString().trim() ||
|
|
231
|
+
throw new Error(result.stderr?.toString().trim() || `${tmuxCommand} ${args.join(" ")} failed`);
|
|
232
232
|
}
|
|
233
233
|
function normalizeExactTmuxTarget(sessionTarget: string, env: NodeJS.ProcessEnv, kind: "session" | "option"): string {
|
|
234
234
|
if (sessionTarget.startsWith("$")) return sessionTarget;
|
|
@@ -407,6 +407,13 @@ function psmuxAuthorityEnvironments(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv[]
|
|
|
407
407
|
if (authorities.length > 0)
|
|
408
408
|
return authorities.map(authority => environmentForProviderAuthority(env, authority));
|
|
409
409
|
}
|
|
410
|
+
if (!ambient.viaExplicitOverride && !ambientAvailable) {
|
|
411
|
+
throw new Error(
|
|
412
|
+
"gjc_tmux_provider_unavailable — GJC searched for psmux, pmux, and tmux on PATH. " +
|
|
413
|
+
"Install psmux from https://github.com/psmux/psmux for native Windows support, use WSL with real tmux, " +
|
|
414
|
+
"or set GJC_TMUX_COMMAND (and GJC_PSMUX_COMMAND when selecting a psmux compatibility alias).",
|
|
415
|
+
);
|
|
416
|
+
}
|
|
410
417
|
}
|
|
411
418
|
if (ambient.isPsmux) throw new Error("gjc_tmux_provider_authority_unavailable");
|
|
412
419
|
return [env];
|