@gajae-code/coding-agent 0.18.0 → 0.18.5

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 (86) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/dist/types/cli/stats-cli.d.ts +0 -5
  3. package/dist/types/config/autorouting-tier-map.d.ts +29 -2
  4. package/dist/types/config/model-registry.d.ts +16 -1
  5. package/dist/types/config/settings-schema.d.ts +2 -2
  6. package/dist/types/exa/mcp-client.d.ts +1 -5
  7. package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
  8. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +1 -0
  9. package/dist/types/hashline/parser.d.ts +28 -5
  10. package/dist/types/modes/acp/acp-agent.d.ts +6 -1
  11. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  12. package/dist/types/sdk/broker/lifecycle.d.ts +6 -1
  13. package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
  14. package/dist/types/sdk/broker/runtime.d.ts +20 -0
  15. package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
  16. package/dist/types/sdk/bus/index.d.ts +1 -0
  17. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  18. package/dist/types/sdk/client/client.d.ts +5 -1
  19. package/dist/types/sdk/host/session-runtime.d.ts +7 -0
  20. package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
  21. package/dist/types/session/agent-session.d.ts +2 -0
  22. package/dist/types/task/index.d.ts +0 -1
  23. package/dist/types/tools/bash.d.ts +1 -0
  24. package/dist/types/tools/descriptors.d.ts +5 -0
  25. package/dist/types/tools/eval-backends.d.ts +37 -0
  26. package/dist/types/tools/eval.d.ts +1 -6
  27. package/dist/types/tools/index.d.ts +1 -36
  28. package/dist/types/tools/session-descriptions.d.ts +22 -0
  29. package/package.json +7 -7
  30. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  31. package/src/cli/doctor/managed-link.ts +4 -4
  32. package/src/cli/doctor/repairs.ts +9 -1
  33. package/src/cli/stats-cli.ts +4 -36
  34. package/src/cli-main.ts +4 -8
  35. package/src/cli.ts +4 -8
  36. package/src/commands/commit.ts +2 -2
  37. package/src/commands/sdk.ts +1 -2
  38. package/src/config/autorouting-tier-map.ts +20 -2
  39. package/src/config/model-preset-registry.ts +17 -2
  40. package/src/config/model-profile-activation.ts +3 -1
  41. package/src/config/model-profiles.ts +11 -3
  42. package/src/config/model-registry.ts +45 -17
  43. package/src/config/settings-schema.ts +8 -2
  44. package/src/exa/mcp-client.ts +41 -8
  45. package/src/exec/bash-shell-guardian.ts +214 -48
  46. package/src/gjc-runtime/session-state-sidecar.ts +88 -58
  47. package/src/hashline/execute.ts +51 -3
  48. package/src/hashline/parser.ts +63 -16
  49. package/src/internal-urls/docs-index.generated.ts +25 -25
  50. package/src/modes/acp/acp-agent.ts +136 -20
  51. package/src/modes/controllers/ssh-command-controller.ts +3 -1
  52. package/src/modes/interactive-mode.ts +10 -0
  53. package/src/modes/utils/context-usage.ts +2 -6
  54. package/src/prompts/tools/task.md +8 -27
  55. package/src/sdk/acp/adapter.ts +38 -3
  56. package/src/sdk/broker/doctor-restart.ts +38 -3
  57. package/src/sdk/broker/lifecycle.ts +408 -218
  58. package/src/sdk/broker/recovery-backoff.ts +135 -0
  59. package/src/sdk/broker/runtime.ts +83 -0
  60. package/src/sdk/broker/startup-budget.ts +6 -1
  61. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  62. package/src/sdk/bus/index.ts +308 -22
  63. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  64. package/src/sdk/client/client.ts +53 -35
  65. package/src/sdk/host/session-runtime.ts +99 -3
  66. package/src/sdk/prompt-watchdog.ts +17 -4
  67. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  68. package/src/session/agent-session.ts +79 -17
  69. package/src/session/internal/managed-session-storage.ts +23 -0
  70. package/src/task/index.ts +21 -66
  71. package/src/tools/bash.ts +39 -0
  72. package/src/tools/descriptors.ts +27 -1
  73. package/src/tools/eval-backends.ts +95 -0
  74. package/src/tools/eval.ts +3 -15
  75. package/src/tools/index.ts +9 -88
  76. package/src/tools/search.ts +2 -7
  77. package/src/tools/session-descriptions.ts +135 -0
  78. package/src/tools/tool-catalog.generated.ts +1 -1
  79. package/dist/types/cli/grep-cli.d.ts +0 -17
  80. package/dist/types/cli/shell-cli.d.ts +0 -8
  81. package/dist/types/commands/grep.d.ts +0 -42
  82. package/dist/types/commands/shell.d.ts +0 -21
  83. package/src/cli/grep-cli.ts +0 -160
  84. package/src/cli/shell-cli.ts +0 -176
  85. package/src/commands/grep.ts +0 -48
  86. package/src/commands/shell.ts +0 -29
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Session-dependent descriptions for tools whose prompt text varies with session settings.
3
+ *
4
+ * A discoverable tool is advertised before its implementation loads. The facade renders these
5
+ * descriptions from the session itself, so the text sent before the first call is the text the
6
+ * loaded tool reports afterwards, and the provider-visible `tools` block (and with it the
7
+ * prompt-cache prefix) does not change when the implementation loads (#5992).
8
+ */
9
+ import { parseFrontmatter, prompt } from "@gajae-code/utils";
10
+ import architectAgent from "../prompts/agents/architect.md" with { type: "text" };
11
+ import criticAgent from "../prompts/agents/critic.md" with { type: "text" };
12
+ import executorAgent from "../prompts/agents/executor.md" with { type: "text" };
13
+ import plannerAgent from "../prompts/agents/planner.md" with { type: "text" };
14
+ import evalDescription from "../prompts/tools/eval.md" with { type: "text" };
15
+ import searchDescription from "../prompts/tools/search.md" with { type: "text" };
16
+ import taskDescription from "../prompts/tools/task.md" with { type: "text" };
17
+ import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "../task/simple-mode";
18
+ import { getTaskSchema, type TaskToolSchemaInstance } from "../task/types";
19
+ import { resolveFileDisplayMode } from "../utils/file-display-mode";
20
+ import { resolveEvalBackends } from "./eval-backends";
21
+
22
+ interface DescriptionSettings {
23
+ get(
24
+ key:
25
+ | "task.maxConcurrency"
26
+ | "task.isolation.mode"
27
+ | "task.simple"
28
+ | "task.disabledAgents"
29
+ | "eval.py"
30
+ | "eval.js"
31
+ | "readLineNumbers"
32
+ | "readHashLines"
33
+ | "edit.mode",
34
+ ): unknown;
35
+ getEffectiveAutorouting(): { active: boolean };
36
+ }
37
+
38
+ interface DescriptionSession {
39
+ settings: DescriptionSettings;
40
+ getSessionSpawns?: () => string | null;
41
+ hasEditTool?: boolean;
42
+ }
43
+
44
+ interface BundledAgentName {
45
+ name: string;
46
+ hide?: boolean;
47
+ }
48
+
49
+ function parseBundledAgentName(markdown: string): BundledAgentName {
50
+ const { frontmatter } = parseFrontmatter(markdown, { level: "fatal" });
51
+ const { name, hide } = frontmatter;
52
+ if (typeof name !== "string") {
53
+ throw new Error("Bundled task agent frontmatter must include a name");
54
+ }
55
+ return { name, hide: hide === true };
56
+ }
57
+
58
+ // Read only each role's frontmatter; importing task/agents would also load and render the full prompts.
59
+ const BUNDLED_AGENT_NAMES = [
60
+ parseBundledAgentName(executorAgent),
61
+ parseBundledAgentName(architectAgent),
62
+ parseBundledAgentName(plannerAgent),
63
+ parseBundledAgentName(criticAgent),
64
+ ];
65
+
66
+ export interface EvalToolDescriptionOptions {
67
+ py?: boolean;
68
+ js?: boolean;
69
+ }
70
+
71
+ export function getEvalToolDescription(options: EvalToolDescriptionOptions = {}): string {
72
+ const py = options.py ?? true;
73
+ const js = options.js ?? true;
74
+ return prompt.render(evalDescription, { py, js });
75
+ }
76
+
77
+ export function evalToolDescriptionForSession(session: DescriptionSession | null | undefined): string {
78
+ if (!session) return getEvalToolDescription();
79
+ const backends = resolveEvalBackends({ settings: session.settings } as Parameters<typeof resolveEvalBackends>[0]);
80
+ return getEvalToolDescription({ py: backends.python, js: backends.js });
81
+ }
82
+
83
+ export function searchToolDescriptionForSession(session: DescriptionSession): string {
84
+ const displayMode = resolveFileDisplayMode(session);
85
+ return prompt.render(searchDescription, {
86
+ IS_HL_MODE: displayMode.hashLines,
87
+ IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
88
+ });
89
+ }
90
+
91
+ export function renderTaskDescription(session: DescriptionSession): string {
92
+ const simpleMode = session.settings.get("task.simple") as TaskSimpleMode;
93
+ const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
94
+ const isolationMode = session.settings.get("task.isolation.mode");
95
+ const parentSpawns = session.getSessionSpawns?.() ?? "*";
96
+ const spawningDisabled = parentSpawns === "";
97
+ const disabledSetting = session.settings.get("task.disabledAgents");
98
+ const disabledAgents = Array.isArray(disabledSetting)
99
+ ? disabledSetting.filter((agent): agent is string => typeof agent === "string")
100
+ : [];
101
+ const allowedSpawns =
102
+ parentSpawns === "*"
103
+ ? undefined
104
+ : new Set(
105
+ parentSpawns
106
+ .split(",")
107
+ .map(agent => agent.trim())
108
+ .filter(Boolean),
109
+ );
110
+ const agents = BUNDLED_AGENT_NAMES.filter(
111
+ agent =>
112
+ agent.hide !== true &&
113
+ !disabledAgents.includes(agent.name) &&
114
+ (allowedSpawns === undefined || allowedSpawns.has(agent.name)),
115
+ );
116
+ return prompt.render(taskDescription, {
117
+ agents: agents.map(({ name }) => ({ name })),
118
+ spawningDisabled,
119
+ MAX_CONCURRENCY: session.settings.get("task.maxConcurrency"),
120
+ isolationEnabled: isolationMode !== "none",
121
+ asyncEnabled: true,
122
+ contextEnabled,
123
+ customSchemaEnabled,
124
+ defaultMode: simpleMode === "default",
125
+ schemaFreeMode: simpleMode === "schema-free",
126
+ independentMode: simpleMode === "independent",
127
+ autoroutingActive: session.settings.getEffectiveAutorouting().active,
128
+ });
129
+ }
130
+
131
+ export function taskParametersForSession(session?: DescriptionSession): TaskToolSchemaInstance {
132
+ const simpleMode = session ? (session.settings.get("task.simple") as TaskSimpleMode) : "default";
133
+ const isolationMode = session?.settings.get("task.isolation.mode");
134
+ return getTaskSchema({ isolationEnabled: session !== undefined && isolationMode !== "none", simpleMode });
135
+ }
@@ -2095,7 +2095,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2095
2095
  "task": {
2096
2096
  "name": "task",
2097
2097
  "label": "Task",
2098
- "description": "Launches subagents to parallelize workflows.\n\n- Results are delivered automatically when complete.\n- The tool result lists the assigned task ids (e.g. `0-AuthLoader`) — those are the live agent ids.\n- Use `subagent` action `inspect` or `list` to snapshot manager state.\n- To wait or cancel, use the `subagent` tool; its await/cancel doctrine is authoritative.\n\nSubagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in `context` or `assignment`.\n\n<parameters>\n- `agent`: agent type for all tasks\n- `tasks`: tasks to execute in parallel\n - `.id`: filesystem-safe, ≤48 chars, matching `[A-Za-z0-9][A-Za-z0-9_-]*`; prefer CamelCase\n - `.description`: UI label only — subagent never sees it\n - `.assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED\n- `context`: shared background prepended to every assignment; session-specific only\n- `.inheritContext` (optional): fork-context mode for seeding the subagent with sanitized parent conversation. Omit it or set `\"none\"` for no copied context. `\"receipt\"` copies a minimal receipt-sized snapshot, `\"last-turn\"` copies only the latest exchange, `\"bounded\"` copies the bounded default snapshot, and `\"full\"` copies a larger snapshot up to the configured/model token cap. Non-`none` modes work only when global `task.forkContext.enabled` is true and the target agent declares `forkContext: allowed`; otherwise the call is rejected. Bundled agents that support it: `executor`, `architect`. Use inherited context only when the subagent's value depends on parent context; cloned tokens are billed to the child as fresh input and surfaced in task receipts as fork-context cloned-token accounting.\n\n- `schema`: JTD schema for expected structured output (do not put format rules in assignments)\n- `spawnPlan` (optional): required before any batch with more than 4 tasks; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.\n\n- `repositoryBinding`: optional authoritative repository/worktree identity. It must match the active session and cannot authorize a foreign repository from this session. For another approved repository, start a fresh session rooted there (for example, `gjc --cwd <approved-worktree>`); do not use `/move` because materialized tasks retain their original binding.\n</parameters>\n\n<rules>\n- HARD runtime gate: calls with more than 4 tasks are rejected before any child launches unless `spawnPlan` is complete.\n- NEVER assign tasks to run project-wide build/test/lint. Caller verifies after the batch.\n- **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates and formatters. You run them once at the end across the union of changed files — avoids redundant runs and racing formatter passes.\n- Each task: ≤3–5 explicit files. No globs, no \"update all\", no package-wide scope. Fan out to a cluster instead.\n- Pass large payloads via `local://<path>` URIs, not inline.\n- Put shared constraints in `context` once; do not duplicate across assignments.\n- Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.\n</rules>\n\n<parallelization>\nTest: can task B run correctly without seeing A's output? If no, sequence A → B.\nSequential when one task produces a contract (types, API, schema, core module) the other consumes.\nParallel when tasks touch disjoint files or are independent refactors/tests.\n</parallelization>\n\n<context-fmt>\n# Goal ← one sentence: what the batch accomplishes\n# Constraints ← MUST/NEVER rules and session decisions\n# Contract ← exact types/signatures if tasks share an interface\n</context-fmt>\n\n<assignment-fmt>\n# Target ← exact files and symbols; explicit non-goals\n# Change ← step-by-step add/remove/rename; APIs and patterns\n# Acceptance ← observable result; no project-wide commands\n</assignment-fmt>\n\n<agents>\n# executor\nAutonomous implementation agent for bounded code changes, fixes, and verification-ready edits\n\n# architect\nRead-only architecture and code-review agent with severity-rated findings and status verdicts\n\n# planner\nRead-only planning agent for sequencing, acceptance criteria, risks, and handoff shape\n\n# critic\nRead-only plan critic that approves only actionable, verifiable execution plans\n</agents>",
2098
+ "description": "Launches subagents to parallelize workflows.\n\n- Results are delivered automatically when complete.\n- The tool result lists the assigned task ids (e.g. `0-AuthLoader`) — those are the live agent ids.\n- Coordinate with running tasks via `irc` when available, using those ids. `subagent` cancel terminates a task and **cannot carry a message**.\n- Use `subagent` action `inspect` or `list` to snapshot manager state.\n- To wait or cancel, use the `subagent` tool; its await/cancel doctrine is authoritative.\n\nSubagents have no conversation history. Every fact, file path, and direction they need MUST be explicit in `context` or `assignment`.\nUse `irc` for live coordination when it is available.\n\n<parameters>\n- `agent`: agent type for all tasks\n- `tasks`: tasks to execute in parallel\n - `.id`: filesystem-safe, ≤48 chars, matching `[A-Za-z0-9][A-Za-z0-9_-]*`; prefer CamelCase\n - `.description`: UI label only — subagent never sees it\n - `.assignment`: complete self-contained instructions; one-liners and missing acceptance criteria are PROHIBITED\n- `context`: shared background prepended to every assignment; session-specific only\n- `.inheritContext` (optional): fork-context mode for seeding the subagent with sanitized parent conversation. Omit it or set `\"none\"` for no copied context. `\"receipt\"` copies a minimal receipt-sized snapshot, `\"last-turn\"` copies only the latest exchange, `\"bounded\"` copies the bounded default snapshot, and `\"full\"` copies a larger snapshot up to the configured/model token cap. Non-`none` modes work only when global `task.forkContext.enabled` is true and the target agent declares `forkContext: allowed`; otherwise the call is rejected. Bundled agents that support it: `executor`, `architect`. Use inherited context only when the subagent's value depends on parent context; cloned tokens are billed to the child as fresh input and surfaced in task receipts as fork-context cloned-token accounting.\n\n- `schema`: JTD schema for expected structured output (do not put format rules in assignments)\n- `spawnPlan` (optional): required before any batch with more than 4 tasks; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.\n\n- `repositoryBinding`: optional authoritative repository/worktree identity. It must match the active session and cannot authorize a foreign repository from this session. For another approved repository, start a fresh session rooted there (for example, `gjc --cwd <approved-worktree>`); do not use `/move` because materialized tasks retain their original binding.\n</parameters>\n\n<rules>\n- HARD runtime gate: calls with more than 4 tasks are rejected before any child launches unless `spawnPlan` is complete.\n- NEVER assign tasks to run project-wide build/test/lint. Caller verifies after the batch.\n- **Subagents do not verify, lint, or format.** Every assignment MUST instruct the subagent to skip all gates and formatters. You run them once at the end across the union of changed files — avoids redundant runs and racing formatter passes.\n- Each task: ≤3–5 explicit files. No globs, no \"update all\", no package-wide scope. Use `irc` to coordinate overlapping work when it is available; otherwise keep file sets independent.\n- Pass large payloads via `local://<path>` URIs, not inline.\n- Put shared constraints in `context` once; do not duplicate across assignments.\n- Prefer agents that investigate **and** edit in one pass; only spin a read-only discovery step when affected files are genuinely unknown.\n</rules>\n\n<parallelization>\nTest: can task B run correctly without seeing A's output? If no, sequence A → B, unless `irc` is available and A can answer a small clarification.\nStill sequence when one task produces a large, evolving contract (generated types, schema migration, core module API); live coordination is not a substitute for a finished contract.\nParallel when tasks touch disjoint files, are independent refactors/tests, or need only occasional peer clarification.\n</parallelization>\n\n<context-fmt>\n# Goal ← one sentence: what the batch accomplishes\n# Constraints ← MUST/NEVER rules and session decisions\n# Contract ← exact types/signatures if tasks share an interface\n</context-fmt>\n\n<assignment-fmt>\n# Target ← exact files and symbols; explicit non-goals\n# Change ← step-by-step add/remove/rename; APIs and patterns\n# Acceptance ← observable result; no project-wide commands\n</assignment-fmt>\n\n<agents>\nBundled role names: executor, architect, planner, critic. A configured agent may override a bundled role name and takes precedence.\nOther configured agents (project, user, plugin) may also be available; calling with an unknown `agent` lists the agents callable in this session.\n</agents>",
2099
2099
  "parameters": {
2100
2100
  "type": "object",
2101
2101
  "properties": {
@@ -1,17 +0,0 @@
1
- import { GrepOutputMode } from "@gajae-code/natives";
2
- export interface GrepCommandArgs {
3
- pattern: string;
4
- path: string;
5
- glob?: string;
6
- limit: number;
7
- context: number;
8
- mode: GrepOutputMode;
9
- gitignore: boolean;
10
- }
11
- /**
12
- * Parse grep subcommand arguments.
13
- * Returns undefined if not a grep command.
14
- */
15
- export declare function parseGrepArgs(args: string[]): GrepCommandArgs | undefined;
16
- export declare function runGrepCommand(cmd: GrepCommandArgs): Promise<void>;
17
- export declare function printGrepHelp(): void;
@@ -1,8 +0,0 @@
1
- export interface ShellCommandArgs {
2
- cwd?: string;
3
- timeoutMs?: number;
4
- noSnapshot?: boolean;
5
- }
6
- export declare function parseShellArgs(args: string[]): ShellCommandArgs | undefined;
7
- export declare function runShellCommand(cmd: ShellCommandArgs): Promise<void>;
8
- export declare function printShellHelp(): void;
@@ -1,42 +0,0 @@
1
- import { Command } from "@gajae-code/utils/cli";
2
- export default class Grep extends Command {
3
- static description: string;
4
- static args: {
5
- pattern: import("@gajae-code/utils/cli").ArgDescriptor & {
6
- description: string;
7
- required: false;
8
- };
9
- path: import("@gajae-code/utils/cli").ArgDescriptor & {
10
- description: string;
11
- required: false;
12
- };
13
- };
14
- static flags: {
15
- glob: import("@gajae-code/utils/cli").FlagDescriptor<"string"> & {
16
- char: string;
17
- description: string;
18
- };
19
- limit: import("@gajae-code/utils/cli").FlagDescriptor<"integer"> & {
20
- char: string;
21
- description: string;
22
- default: number;
23
- };
24
- context: import("@gajae-code/utils/cli").FlagDescriptor<"integer"> & {
25
- char: string;
26
- description: string;
27
- default: number;
28
- };
29
- files: import("@gajae-code/utils/cli").FlagDescriptor<"boolean"> & {
30
- char: string;
31
- description: string;
32
- };
33
- count: import("@gajae-code/utils/cli").FlagDescriptor<"boolean"> & {
34
- char: string;
35
- description: string;
36
- };
37
- "no-gitignore": import("@gajae-code/utils/cli").FlagDescriptor<"boolean"> & {
38
- description: string;
39
- };
40
- };
41
- run(): Promise<void>;
42
- }
@@ -1,21 +0,0 @@
1
- /**
2
- * Interactive shell console.
3
- */
4
- import { Command } from "@gajae-code/utils/cli";
5
- export default class Shell extends Command {
6
- static description: string;
7
- static flags: {
8
- cwd: import("@gajae-code/utils/cli").FlagDescriptor<"string"> & {
9
- char: string;
10
- description: string;
11
- };
12
- timeout: import("@gajae-code/utils/cli").FlagDescriptor<"integer"> & {
13
- char: string;
14
- description: string;
15
- };
16
- "no-snapshot": import("@gajae-code/utils/cli").FlagDescriptor<"boolean"> & {
17
- description: string;
18
- };
19
- };
20
- run(): Promise<void>;
21
- }
@@ -1,160 +0,0 @@
1
- /**
2
- * Grep CLI command handlers.
3
- *
4
- * Handles `gjc grep` subcommand for testing grep tool on Windows.
5
- */
6
- import * as path from "node:path";
7
- import { GrepOutputMode, grep } from "@gajae-code/natives";
8
- import { APP_NAME } from "@gajae-code/utils";
9
- import chalk from "chalk";
10
-
11
- export interface GrepCommandArgs {
12
- pattern: string;
13
- path: string;
14
- glob?: string;
15
- limit: number;
16
- context: number;
17
- mode: GrepOutputMode;
18
- gitignore: boolean;
19
- }
20
-
21
- /**
22
- * Parse grep subcommand arguments.
23
- * Returns undefined if not a grep command.
24
- */
25
- export function parseGrepArgs(args: string[]): GrepCommandArgs | undefined {
26
- if (args.length === 0 || args[0] !== "grep") {
27
- return undefined;
28
- }
29
-
30
- const result: GrepCommandArgs = {
31
- pattern: "",
32
- path: ".",
33
- limit: 20,
34
- context: 2,
35
- mode: GrepOutputMode.Content,
36
- gitignore: true,
37
- };
38
-
39
- const positional: string[] = [];
40
-
41
- for (let i = 1; i < args.length; i++) {
42
- const arg = args[i];
43
- if (arg === "--glob" || arg === "-g") {
44
- result.glob = args[++i];
45
- } else if (arg === "--limit" || arg === "-l") {
46
- result.limit = parseInt(args[++i], 10);
47
- } else if (arg === "--context" || arg === "-C") {
48
- result.context = parseInt(args[++i], 10);
49
- } else if (arg === "--files" || arg === "-f") {
50
- result.mode = GrepOutputMode.FilesWithMatches;
51
- } else if (arg === "--count" || arg === "-c") {
52
- result.mode = GrepOutputMode.Count;
53
- } else if (arg === "--no-gitignore") {
54
- result.gitignore = false;
55
- } else if (!arg.startsWith("-")) {
56
- positional.push(arg);
57
- }
58
- }
59
-
60
- if (positional.length >= 1) {
61
- result.pattern = positional[0];
62
- }
63
- if (positional.length >= 2) {
64
- result.path = positional[1];
65
- }
66
-
67
- return result;
68
- }
69
-
70
- export async function runGrepCommand(cmd: GrepCommandArgs): Promise<void> {
71
- if (!cmd.pattern) {
72
- console.error(chalk.red("Error: Pattern is required"));
73
- process.exit(1);
74
- }
75
-
76
- const searchPath = path.resolve(cmd.path);
77
- console.log(chalk.dim(`Searching in: ${searchPath}`));
78
- console.log(chalk.dim(`Pattern: ${cmd.pattern}`));
79
- console.log(
80
- chalk.dim(`Mode: ${cmd.mode}, Limit: ${cmd.limit}, Context: ${cmd.context}, Gitignore: ${cmd.gitignore}`),
81
- );
82
-
83
- console.log("");
84
-
85
- try {
86
- const result = await grep({
87
- pattern: cmd.pattern,
88
- path: searchPath,
89
- glob: cmd.glob,
90
- mode: cmd.mode,
91
- maxCount: cmd.limit,
92
- context: cmd.mode === GrepOutputMode.Content ? cmd.context : undefined,
93
- hidden: true,
94
- gitignore: cmd.gitignore,
95
- });
96
-
97
- console.log(chalk.green(`Total matches: ${result.totalMatches}`));
98
- console.log(chalk.green(`Files with matches: ${result.filesWithMatches}`));
99
- console.log(chalk.green(`Files searched: ${result.filesSearched}`));
100
- if (result.limitReached) {
101
- console.log(chalk.yellow(`Result limit reached (${cmd.limit} matches). Use --limit to show more.`));
102
- }
103
- console.log("");
104
-
105
- for (const match of result.matches) {
106
- const displayPath = match.path.replace(/\\/g, "/");
107
-
108
- if (cmd.mode === GrepOutputMode.Content) {
109
- if (match.contextBefore) {
110
- for (const ctx of match.contextBefore) {
111
- console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
112
- }
113
- }
114
- console.log(`${chalk.cyan(displayPath)}:${chalk.yellow(String(match.lineNumber))}: ${match.line}`);
115
- if (match.contextAfter) {
116
- for (const ctx of match.contextAfter) {
117
- console.log(chalk.dim(`${displayPath}-${ctx.lineNumber}- ${ctx.line}`));
118
- }
119
- }
120
- console.log("");
121
- } else if (cmd.mode === GrepOutputMode.Count) {
122
- console.log(`${chalk.cyan(displayPath)}: ${match.matchCount ?? 0} matches`);
123
- } else {
124
- console.log(chalk.cyan(displayPath));
125
- }
126
- }
127
- } catch (err) {
128
- console.error(chalk.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
129
- process.exit(1);
130
- }
131
- }
132
-
133
- export function printGrepHelp(): void {
134
- console.log(`${chalk.bold(`${APP_NAME} grep`)} - Test grep tool
135
-
136
- ${chalk.bold("Usage:")}
137
- ${APP_NAME} grep <pattern> [path] [options]
138
-
139
- ${chalk.bold("Arguments:")}
140
- pattern Regex pattern to search for
141
- path Directory or file to search (default: .)
142
-
143
- ${chalk.bold("Options:")}
144
- -g, --glob <pattern> Filter files by glob pattern
145
- -l, --limit <n> Max matches (default: 20)
146
- -C, --context <n> Context lines (default: 2)
147
- -f, --files Output file names only
148
- -c, --count Output match counts per file
149
- -h, --help Show this help
150
- --no-gitignore Include files excluded by .gitignore
151
-
152
- ${chalk.bold("Environment:")}
153
- PI_GREP_WORKERS=N Set filesystem walker workers (default 4, 0 = auto)
154
-
155
- ${chalk.bold("Examples:")}
156
- ${APP_NAME} grep "import" src/
157
- ${APP_NAME} grep "TODO" . --glob "*.ts"
158
- ${APP_NAME} grep "function" --files
159
- `);
160
- }
@@ -1,176 +0,0 @@
1
- /**
2
- * Shell CLI command handlers.
3
- *
4
- * Handles `gjc shell` subcommand for testing the native brush-core shell.
5
- */
6
- import * as path from "node:path";
7
- import { createInterface } from "node:readline/promises";
8
- import { Shell } from "@gajae-code/natives";
9
- import { APP_NAME, getProjectDir } from "@gajae-code/utils";
10
- import chalk from "chalk";
11
- import { Settings } from "../config/settings";
12
- import { buildMinimizerOptions } from "../exec/bash-executor";
13
- import { getOrCreateSnapshot } from "../utils/shell-snapshot";
14
-
15
- export interface ShellCommandArgs {
16
- cwd?: string;
17
- timeoutMs?: number;
18
- noSnapshot?: boolean;
19
- }
20
-
21
- export function parseShellArgs(args: string[]): ShellCommandArgs | undefined {
22
- if (args.length === 0 || args[0] !== "shell") {
23
- return undefined;
24
- }
25
-
26
- const result: ShellCommandArgs = {};
27
-
28
- for (let i = 1; i < args.length; i++) {
29
- const arg = args[i];
30
- if (arg === "--cwd" || arg === "-C") {
31
- result.cwd = args[++i];
32
- } else if (arg === "--timeout" || arg === "-t") {
33
- const parsed = Number.parseInt(args[++i], 10);
34
- if (Number.isFinite(parsed)) {
35
- result.timeoutMs = parsed;
36
- }
37
- } else if (arg === "--no-snapshot") {
38
- result.noSnapshot = true;
39
- }
40
- }
41
-
42
- return result;
43
- }
44
-
45
- export async function runShellCommand(cmd: ShellCommandArgs): Promise<void> {
46
- if (!process.stdin.isTTY) {
47
- process.stderr.write("Error: shell console requires an interactive TTY.\n");
48
- process.exit(1);
49
- }
50
-
51
- const cwd = cmd.cwd ? path.resolve(cmd.cwd) : getProjectDir();
52
- const settings = await Settings.init({ cwd });
53
- const { shell, env: shellEnv } = settings.getShellConfig();
54
- const snapshotPath = cmd.noSnapshot || !shell.includes("bash") ? null : await getOrCreateSnapshot(shell, shellEnv);
55
- const minimizer = buildMinimizerOptions(settings.getGroup("shellMinimizer"));
56
- const shellSession = new Shell({ sessionEnv: shellEnv, snapshotPath: snapshotPath ?? undefined, minimizer });
57
-
58
- let active = false;
59
- let lastChar: string | null = null;
60
-
61
- const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
62
- const prompt = chalk.cyan(`${APP_NAME} shell> `);
63
-
64
- const printHelp = () => {
65
- process.stdout.write(
66
- `${chalk.bold("Shell Console Commands")}
67
-
68
- ` +
69
- `${chalk.bold("Special Commands:")}
70
- .help Show this help
71
- .exit, exit Exit the console
72
-
73
- ` +
74
- `${chalk.bold("Options:")}
75
- --cwd, -C <path> Set working directory for commands
76
- --timeout, -t <ms> Timeout per command in milliseconds
77
- --no-snapshot Skip sourcing snapshot from user shell
78
-
79
- ` +
80
- `${chalk.bold("Notes:")}
81
- Runs in a persistent brush-core shell session.
82
- Variables and functions defined in one command persist for the next.
83
-
84
- `,
85
- );
86
- };
87
-
88
- const interruptHandler = () => {
89
- if (active) {
90
- void shellSession.abort();
91
- return;
92
- }
93
- rl.close();
94
- process.exit(0);
95
- };
96
-
97
- process.on("SIGINT", interruptHandler);
98
- process.stdout.write(chalk.dim("Type .help for commands.\n"));
99
-
100
- try {
101
- while (true) {
102
- const line = (await rl.question(prompt)).trim();
103
- if (!line) {
104
- continue;
105
- }
106
- if (line === ".help") {
107
- printHelp();
108
- continue;
109
- }
110
- if (line === ".exit" || line === "exit" || line === "quit") {
111
- break;
112
- }
113
-
114
- active = true;
115
- lastChar = null;
116
- try {
117
- const result = await shellSession.run(
118
- {
119
- command: line,
120
- cwd,
121
- timeoutMs: cmd.timeoutMs,
122
- },
123
- (err, chunk) => {
124
- if (err) {
125
- process.stderr.write(`${err.message}\n`);
126
- return;
127
- }
128
- if (chunk.length > 0) {
129
- lastChar = chunk[chunk.length - 1] ?? null;
130
- }
131
- process.stdout.write(chunk);
132
- },
133
- );
134
-
135
- if (lastChar && lastChar !== "\n") {
136
- process.stdout.write("\n");
137
- }
138
-
139
- if (result.timedOut) {
140
- process.stderr.write(chalk.yellow("Command timed out.\n"));
141
- } else if (result.cancelled) {
142
- process.stderr.write(chalk.yellow("Command cancelled.\n"));
143
- } else if (result.exitCode !== 0 && result.exitCode !== undefined) {
144
- process.stderr.write(chalk.yellow(`Exit code: ${result.exitCode}\n`));
145
- }
146
- } catch (err) {
147
- const message = err instanceof Error ? err.message : String(err);
148
- process.stderr.write(chalk.red(`Error: ${message}\n`));
149
- } finally {
150
- active = false;
151
- }
152
- }
153
- } finally {
154
- process.off("SIGINT", interruptHandler);
155
- rl.close();
156
- }
157
- }
158
-
159
- export function printShellHelp(): void {
160
- process.stdout.write(`${chalk.bold(`${APP_NAME} shell`)} - Interactive shell console for testing
161
-
162
- ${chalk.bold("Usage:")}
163
- ${APP_NAME} shell [options]
164
-
165
- ${chalk.bold("Options:")}
166
- --cwd, -C <path> Set working directory for commands
167
- --timeout, -t <ms> Timeout per command in milliseconds
168
- --no-snapshot Skip sourcing snapshot from user shell
169
- -h, --help Show this help
170
-
171
- ${chalk.bold("Examples:")}
172
- ${APP_NAME} shell
173
- ${APP_NAME} shell --cwd ./tmp
174
- ${APP_NAME} shell --timeout 2000
175
- `);
176
- }
@@ -1,48 +0,0 @@
1
- /**
2
- * Test grep tool.
3
- */
4
- import { GrepOutputMode } from "@gajae-code/natives";
5
- import { Args, Command, Flags } from "@gajae-code/utils/cli";
6
- import { type GrepCommandArgs, runGrepCommand } from "../cli/grep-cli";
7
- import { initTheme } from "../modes/theme/theme";
8
-
9
- export default class Grep extends Command {
10
- static description = "Test grep tool";
11
-
12
- static args = {
13
- pattern: Args.string({ description: "Regex pattern to search for", required: false }),
14
- path: Args.string({ description: "Directory or file to search", required: false }),
15
- };
16
-
17
- static flags = {
18
- glob: Flags.string({ char: "g", description: "Filter files by glob pattern" }),
19
- limit: Flags.integer({ char: "l", description: "Max matches", default: 20 }),
20
- context: Flags.integer({ char: "C", description: "Context lines", default: 2 }),
21
- files: Flags.boolean({ char: "f", description: "Output file names only" }),
22
- count: Flags.boolean({ char: "c", description: "Output match counts per file" }),
23
- "no-gitignore": Flags.boolean({ description: "Include files excluded by .gitignore" }),
24
- };
25
-
26
- async run(): Promise<void> {
27
- const { args, flags } = await this.parse(Grep);
28
-
29
- const mode: GrepCommandArgs["mode"] = flags.count
30
- ? GrepOutputMode.Count
31
- : flags.files
32
- ? GrepOutputMode.FilesWithMatches
33
- : GrepOutputMode.Content;
34
-
35
- const cmd: GrepCommandArgs = {
36
- pattern: args.pattern ?? "",
37
- path: args.path ?? ".",
38
- glob: flags.glob,
39
- limit: flags.limit,
40
- context: flags.context,
41
- mode,
42
- gitignore: !flags["no-gitignore"],
43
- };
44
-
45
- await initTheme();
46
- await runGrepCommand(cmd);
47
- }
48
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Interactive shell console.
3
- */
4
- import { Command, Flags } from "@gajae-code/utils/cli";
5
- import { runShellCommand, type ShellCommandArgs } from "../cli/shell-cli";
6
- import { initTheme } from "../modes/theme/theme";
7
-
8
- export default class Shell extends Command {
9
- static description = "Interactive shell console";
10
-
11
- static flags = {
12
- cwd: Flags.string({ char: "C", description: "Set working directory for commands" }),
13
- timeout: Flags.integer({ char: "t", description: "Timeout per command in milliseconds" }),
14
- "no-snapshot": Flags.boolean({ description: "Skip sourcing snapshot from user shell" }),
15
- };
16
-
17
- async run(): Promise<void> {
18
- const { flags } = await this.parse(Shell);
19
-
20
- const cmd: ShellCommandArgs = {
21
- cwd: flags.cwd,
22
- timeoutMs: flags.timeout,
23
- noSnapshot: flags["no-snapshot"],
24
- };
25
-
26
- await initTheme();
27
- await runShellCommand(cmd);
28
- }
29
- }