@gajae-code/coding-agent 0.12.8 → 0.12.10

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/cli/notify-cli.d.ts +1 -0
  3. package/dist/types/commands/sdk.d.ts +23 -0
  4. package/dist/types/discovery/agents-md.d.ts +11 -1
  5. package/dist/types/modes/types.d.ts +6 -0
  6. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  7. package/dist/types/sdk/bus/notification-service.d.ts +23 -0
  8. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -2
  9. package/dist/types/session/internal/managed-session-storage.d.ts +8 -0
  10. package/dist/types/session/internal/native-publish-outcome.d.ts +2 -2
  11. package/dist/types/system-prompt.d.ts +12 -1
  12. package/dist/types/tools/composer-bash-policy.d.ts +9 -1
  13. package/package.json +8 -8
  14. package/scripts/verify-gjc-sdk-canonicalization.ts +14 -1
  15. package/src/cli/notify-cli.ts +12 -6
  16. package/src/commands/sdk.ts +54 -0
  17. package/src/config/model-profiles.ts +8 -0
  18. package/src/config/model-registry.ts +4 -1
  19. package/src/discovery/agents-md.ts +76 -19
  20. package/src/gjc-runtime/psmux-detect.ts +12 -16
  21. package/src/gjc-runtime/tmux-sessions.ts +8 -1
  22. package/src/internal-urls/docs-index.generated.ts +4 -3
  23. package/src/modes/components/extensions/inspector-panel.ts +9 -5
  24. package/src/modes/controllers/selector-controller.ts +61 -43
  25. package/src/modes/print-mode.ts +3 -0
  26. package/src/modes/types.ts +6 -0
  27. package/src/prompts/system/system-prompt.md +1 -0
  28. package/src/sdk/bus/daemon-paths.ts +2 -0
  29. package/src/sdk/bus/notification-service.ts +140 -7
  30. package/src/sdk/bus/telegram-daemon-contract.ts +2 -2
  31. package/src/sdk/session.ts +24 -7
  32. package/src/session/agent-session.ts +65 -13
  33. package/src/session/internal/managed-session-storage.ts +36 -2
  34. package/src/session/internal/native-publish-outcome.ts +12 -2
  35. package/src/slash-commands/builtin-registry.ts +35 -11
  36. package/src/system-prompt.ts +28 -15
  37. package/src/tools/bash.ts +3 -1
  38. package/src/tools/composer-bash-policy.ts +21 -5
  39. package/src/utils/command-args.ts +19 -30
  40. package/src/utils/lang-from-path.ts +2 -4
@@ -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(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
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 content = await readFile(candidate);
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
- if (!baseName.startsWith(".")) {
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: calculatedDepth,
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
- if (current === (ctx.repoRoot ?? ctx.home)) break; // scanned repo root or home, stop
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
- // Move to parent directory
53
- const parent = path.dirname(current);
54
- if (parent === current) break; // Reached filesystem root
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 candidates: Array<{ command: string; identity: string }> = [];
328
- for (const candidate of PSMUX_BINARY_NAMES) {
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
- const isPsmuxAlias = candidate !== "tmux" || detectPsmuxForCommand(candidate, runner);
332
- if (!isPsmuxAlias) continue;
333
- const identity = activeExecutableIdentityResolver(executablePath);
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
- if (candidates.length > 0) {
339
- const identities = new Set(candidates.map(candidate => candidate.identity));
340
- if (identities.size !== 1)
341
- throw new Error("gjc_tmux_provider_ambiguous: Windows psmux aliases resolve to different executables");
342
- // The order is the canonical provider spelling, then its compatibility
343
- // aliases. Identity equality above makes PATH-order alias selection safe.
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() || `tmux ${args.join(" ")} failed`);
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];