@gajae-code/coding-agent 0.18.1 → 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 (55) hide show
  1. package/CHANGELOG.md +60 -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/exa/mcp-client.d.ts +1 -5
  6. package/dist/types/exec/bash-shell-guardian.d.ts +26 -0
  7. package/dist/types/hashline/parser.d.ts +28 -5
  8. package/dist/types/modes/acp/acp-agent.d.ts +6 -1
  9. package/dist/types/sdk/broker/lifecycle.d.ts +6 -1
  10. package/dist/types/sdk/broker/recovery-backoff.d.ts +62 -0
  11. package/dist/types/sdk/broker/runtime.d.ts +20 -0
  12. package/dist/types/sdk/broker/startup-budget.d.ts +5 -1
  13. package/dist/types/sdk/bus/index.d.ts +1 -0
  14. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  15. package/dist/types/sdk/host/session-runtime.d.ts +7 -0
  16. package/dist/types/sdk/prompt-watchdog.d.ts +1 -1
  17. package/dist/types/tools/bash.d.ts +1 -0
  18. package/package.json +7 -7
  19. package/src/cli/stats-cli.ts +4 -36
  20. package/src/cli-main.ts +4 -8
  21. package/src/cli.ts +4 -8
  22. package/src/commands/commit.ts +2 -2
  23. package/src/commands/sdk.ts +1 -2
  24. package/src/config/autorouting-tier-map.ts +20 -2
  25. package/src/config/model-preset-registry.ts +17 -2
  26. package/src/config/model-profile-activation.ts +3 -1
  27. package/src/config/model-profiles.ts +11 -3
  28. package/src/config/model-registry.ts +45 -17
  29. package/src/exa/mcp-client.ts +41 -8
  30. package/src/exec/bash-shell-guardian.ts +184 -34
  31. package/src/hashline/execute.ts +51 -3
  32. package/src/hashline/parser.ts +63 -16
  33. package/src/internal-urls/docs-index.generated.ts +22 -22
  34. package/src/modes/acp/acp-agent.ts +39 -4
  35. package/src/modes/controllers/ssh-command-controller.ts +3 -1
  36. package/src/sdk/broker/lifecycle.ts +408 -218
  37. package/src/sdk/broker/recovery-backoff.ts +135 -0
  38. package/src/sdk/broker/runtime.ts +83 -0
  39. package/src/sdk/broker/startup-budget.ts +6 -1
  40. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  41. package/src/sdk/bus/index.ts +305 -19
  42. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  43. package/src/sdk/host/session-runtime.ts +99 -3
  44. package/src/sdk/prompt-watchdog.ts +17 -4
  45. package/src/session/agent-session.ts +14 -2
  46. package/src/session/internal/managed-session-storage.ts +23 -0
  47. package/src/tools/bash.ts +39 -0
  48. package/dist/types/cli/grep-cli.d.ts +0 -17
  49. package/dist/types/cli/shell-cli.d.ts +0 -8
  50. package/dist/types/commands/grep.d.ts +0 -42
  51. package/dist/types/commands/shell.d.ts +0 -21
  52. package/src/cli/grep-cli.ts +0 -160
  53. package/src/cli/shell-cli.ts +0 -176
  54. package/src/commands/grep.ts +0 -48
  55. package/src/commands/shell.ts +0 -29
@@ -50,6 +50,12 @@ import { parseThinkingLevel } from "../../thinking";
50
50
  import { readEndpointFile } from "../broker/endpoint-authority";
51
51
  import { ensureBroker } from "../broker/ensure";
52
52
  import { processIncarnation } from "../broker/process-incarnation";
53
+ import { RecoveryBackoffTracker } from "../broker/recovery-backoff";
54
+ import {
55
+ captureRuntimeImageIdentity,
56
+ isSdkInternalRuntimeImageReplaced,
57
+ sdkInternalRuntimeImage,
58
+ } from "../broker/runtime";
53
59
  import {
54
60
  type MasterRoleAttestationV2,
55
61
  resolveSessionLocator,
@@ -716,6 +722,15 @@ export interface CreateSdkSessionRuntimeOptions {
716
722
  onFailureDiagnosticKeyCountForTests?: (count: number) => void;
717
723
  /** Test-only observation after a resolved invocation completion is reconciled. */
718
724
  onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void;
725
+ /** Test hook: provides a function to inject a controlled clock for broker recovery backoff assertions. */
726
+ setRecoveryBackoffClockForTest?: (inject: (clock: { now(): number }) => void) => void;
727
+ /** Test hook: inject mock runtime image identity and replacement detection for broker recovery tests. */
728
+ setRuntimeImageIdentityForTest?: (
729
+ register: (
730
+ captureFn: () => Awaited<ReturnType<typeof captureRuntimeImageIdentity>> | undefined,
731
+ isReplacedFn: () => Promise<boolean>,
732
+ ) => void,
733
+ ) => void;
719
734
  }
720
735
 
721
736
  function unavailable(operation: string): () => never {
@@ -7013,6 +7028,43 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7013
7028
  let brokerRegistered = false;
7014
7029
  let brokerRegistrationInFlight: Promise<void> | undefined;
7015
7030
  let brokerRecoveryStopped = false;
7031
+ let brokerRecoveryRestartRequired = false;
7032
+ const brokerRecoveryBackoff = new RecoveryBackoffTracker();
7033
+ if (options.setRecoveryBackoffClockForTest) {
7034
+ options.setRecoveryBackoffClockForTest((clock: { now(): number }) =>
7035
+ brokerRecoveryBackoff.setClockForTest(clock),
7036
+ );
7037
+ }
7038
+ // Capture runtime image identity at startup to detect replacements
7039
+ let startupRuntimeImageIdentity: Awaited<ReturnType<typeof captureRuntimeImageIdentity>> | undefined;
7040
+ let captureStartupRuntimeImageForTest:
7041
+ | (() => Awaited<ReturnType<typeof captureRuntimeImageIdentity>> | undefined)
7042
+ | undefined;
7043
+ let isRuntimeImageReplacedForTest: (() => Promise<boolean>) | undefined;
7044
+ if (options.setRuntimeImageIdentityForTest) {
7045
+ options.setRuntimeImageIdentityForTest(
7046
+ (
7047
+ captureFn: () => Awaited<ReturnType<typeof captureRuntimeImageIdentity>> | undefined,
7048
+ isReplacedFn: () => Promise<boolean>,
7049
+ ) => {
7050
+ captureStartupRuntimeImageForTest = captureFn;
7051
+ isRuntimeImageReplacedForTest = isReplacedFn;
7052
+ },
7053
+ );
7054
+ }
7055
+ const captureStartupRuntimeImage = async (): Promise<void> => {
7056
+ if (options.setRuntimeImageIdentityForTest) {
7057
+ // Test mode: use injected identity
7058
+ startupRuntimeImageIdentity = captureStartupRuntimeImageForTest?.();
7059
+ } else {
7060
+ // Production mode: capture real runtime image
7061
+ const runtimeImage = sdkInternalRuntimeImage();
7062
+ if (runtimeImage) {
7063
+ startupRuntimeImageIdentity = await captureRuntimeImageIdentity(runtimeImage);
7064
+ }
7065
+ }
7066
+ };
7067
+ const startupImageCapture = captureStartupRuntimeImage();
7016
7068
  const registerBroker = async (): Promise<void> => {
7017
7069
  if (brokerRegistered) return;
7018
7070
  if (brokerRegistrationInFlight !== undefined) {
@@ -7143,17 +7195,58 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7143
7195
  let brokerRecoveryTimer: NodeJS.Timeout | undefined;
7144
7196
  let brokerRecoveryInFlight: Promise<void> | undefined;
7145
7197
  const runBrokerRecovery = async (): Promise<void> => {
7146
- if (brokerRecoveryStopped || brokerRecoveryInFlight !== undefined) return;
7198
+ if (brokerRecoveryStopped || brokerRecoveryInFlight !== undefined || brokerRecoveryRestartRequired) return;
7199
+ // Checked synchronously so an interval tick cannot start an attempt the backoff forbids.
7200
+ if (!brokerRecoveryBackoff.canAttemptRecovery(options.agentDir)) return;
7201
+
7202
+ // The single-flight slot is claimed before the first await: the interval does not wait
7203
+ // for prior callbacks, so a slow runtime-image probe must not let a second tick through
7204
+ // to ensure the broker and record the same failure twice.
7147
7205
  const recovery = (async (): Promise<void> => {
7206
+ // A replaced runtime image cannot launch a broker; restarting the session is the only fix.
7207
+ const imageReplaced = await (isRuntimeImageReplacedForTest
7208
+ ? isRuntimeImageReplacedForTest()
7209
+ : isSdkInternalRuntimeImageReplaced(startupRuntimeImageIdentity).catch(() => false));
7210
+ if (imageReplaced) {
7211
+ brokerRecoveryRestartRequired = true;
7212
+ stopBrokerRecovery();
7213
+ logger.warn("sdk broker recovery requires session restart", {
7214
+ reason: "runtime_image_replaced",
7215
+ });
7216
+ return;
7217
+ }
7148
7218
  if (brokerRecoveryStopped) return;
7149
7219
  if (brokerRegistered) await (options.ensureBrokerImpl ?? ensureBroker)({ agentDir: options.agentDir });
7150
- else await registerBroker();
7220
+ else {
7221
+ // Optional registration logs and resolves on failure; recovery must still count it
7222
+ // as a failed attempt so the backoff holds instead of resetting every tick.
7223
+ await registerBroker();
7224
+ if (!brokerRegistered && !brokerRecoveryStopped)
7225
+ throw Object.assign(new Error("SDK broker registration did not complete."), {
7226
+ code: "registration_incomplete",
7227
+ });
7228
+ }
7151
7229
  })();
7152
7230
  brokerRecoveryInFlight = recovery;
7153
7231
  try {
7154
7232
  await recovery;
7233
+ if (brokerRecoveryRestartRequired || brokerRecoveryStopped) return;
7234
+ brokerRecoveryBackoff.recordSuccess(options.agentDir);
7155
7235
  } catch (error) {
7156
- logger.warn("sdk broker recovery unavailable", { code: brokerDiagnosticCode(error) });
7236
+ const shouldContinue = brokerRecoveryBackoff.recordFailure(options.agentDir);
7237
+ if (!shouldContinue) {
7238
+ // Max recovery attempts reached; mark that restart is required.
7239
+ brokerRecoveryRestartRequired = true;
7240
+ stopBrokerRecovery();
7241
+ logger.warn("sdk broker recovery max attempts exceeded", {
7242
+ code: brokerDiagnosticCode(error),
7243
+ });
7244
+ } else {
7245
+ logger.warn("sdk broker recovery unavailable", {
7246
+ code: brokerDiagnosticCode(error),
7247
+ backoffWaitMs: brokerRecoveryBackoff.getBackoffWaitMs(options.agentDir),
7248
+ });
7249
+ }
7157
7250
  } finally {
7158
7251
  if (brokerRecoveryInFlight === recovery) brokerRecoveryInFlight = undefined;
7159
7252
  }
@@ -7210,6 +7303,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
7210
7303
  try {
7211
7304
  publishedEndpointUrl = (await runtime.start()).url;
7212
7305
  await registerBroker();
7306
+ // Await the startup runtime image capture before arming recovery.
7307
+ // This ensures replacement detection works correctly on the first recovery check.
7308
+ await startupImageCapture;
7213
7309
  if (!brokerRecoveryStopped) startBrokerRecovery();
7214
7310
  } catch (error) {
7215
7311
  active = undefined;
@@ -97,8 +97,13 @@ export class PromptActivity {
97
97
  #awaitingModel = false;
98
98
 
99
99
  /** Folds one inbound frame's lifecycle event into the in-flight tool and model state. */
100
- observe(eventType: string | undefined, toolCallId: string | undefined, messageRole: string | undefined): void {
101
- this.#observeInference(eventType, messageRole);
100
+ observe(
101
+ eventType: string | undefined,
102
+ toolCallId: string | undefined,
103
+ messageRole: string | undefined,
104
+ messageHasToolCall = false,
105
+ ): void {
106
+ this.#observeInference(eventType, messageRole, messageHasToolCall);
102
107
  if (toolCallId === undefined) return;
103
108
  if (eventType === "tool_execution_start" || eventType === "tool_execution_update") this.#running.add(toolCallId);
104
109
  else if (eventType === "tool_execution_end") this.#running.delete(toolCallId);
@@ -110,7 +115,11 @@ export class PromptActivity {
110
115
  * re-invocation that carries a tool result back — until the first frame of that
111
116
  * response proves the model answered.
112
117
  */
113
- #observeInference(eventType: string | undefined, messageRole: string | undefined): void {
118
+ #observeInference(
119
+ eventType: string | undefined,
120
+ messageRole: string | undefined,
121
+ messageHasToolCall: boolean,
122
+ ): void {
114
123
  if (eventType === "agent_start" || eventType === "tool_execution_end") {
115
124
  this.#awaitingModel = true;
116
125
  return;
@@ -132,7 +141,11 @@ export class PromptActivity {
132
141
  // Providers that do not stream emit no `message_update`, so the assistant message
133
142
  // itself is the first proof. `message_start`/`message_end` also carry the user and
134
143
  // tool-result messages the host echoes back, which prove nothing about the model.
135
- if ((eventType === "message_start" || eventType === "message_end") && messageRole === "assistant")
144
+ if (
145
+ (eventType === "message_start" || eventType === "message_end") &&
146
+ messageRole === "assistant" &&
147
+ !messageHasToolCall
148
+ )
136
149
  this.#awaitingModel = false;
137
150
  }
138
151
 
@@ -24425,8 +24425,15 @@ export class AgentSession {
24425
24425
  if (!managedFallback && assistantMessageHasVisibleOrToolContent(message)) {
24426
24426
  return false;
24427
24427
  }
24428
+ const canReplayUnexpectedSocketClose =
24429
+ classification === "transient" &&
24430
+ isUnexpectedSocketCloseMessage(message.errorMessage ?? "") &&
24431
+ !hasBareDefaultRetryDisqualifyingFacts(message) &&
24432
+ this.#hasCleanRetryReplaySafety;
24428
24433
  // Bare defaults retain their narrow watchdog and provider capacity-overload
24429
- // admissions. A first-event timeout adds the typed, content-free,
24434
+ // admissions. A content-free unexpected socket close is replay-safe when
24435
+ // the attempt has no conflicting facts and the retry scope is clean. A
24436
+ // first-event timeout adds the typed, content-free,
24430
24437
  // current-clean-scope requirement above; other transient watchdogs preserve
24431
24438
  // legacy behavior. A provider overload is admitted only from that provider's
24432
24439
  // own typed overload code on a content-free attempt carrying no conflicting
@@ -24435,12 +24442,16 @@ export class AgentSession {
24435
24442
  if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential && !canReplayEmptyResponse) {
24436
24443
  if (
24437
24444
  (!canReplayProviderOverload &&
24445
+ !canReplayUnexpectedSocketClose &&
24438
24446
  !this.#isTypedFirstEventTimeout(message) &&
24439
24447
  !messageOnlyWatchdogTimeout &&
24440
24448
  (hasBareDefaultRetryDisqualifyingFacts(message) ||
24441
24449
  (classification !== "transient" && classification !== "first_event_timeout") ||
24442
24450
  !BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
24443
- (!firstEventTimeout && !messageOnlyWatchdogTimeout && !this.#hasCleanRetryReplaySafety)
24451
+ (!firstEventTimeout &&
24452
+ !messageOnlyWatchdogTimeout &&
24453
+ !canReplayUnexpectedSocketClose &&
24454
+ !this.#hasCleanRetryReplaySafety)
24444
24455
  ) {
24445
24456
  return false;
24446
24457
  }
@@ -24449,6 +24460,7 @@ export class AgentSession {
24449
24460
  !managedFallback &&
24450
24461
  classification === "transient" &&
24451
24462
  !canReplayProviderOverload &&
24463
+ !canReplayUnexpectedSocketClose &&
24452
24464
  !this.#isIdleStreamStallErrorMessage(message.errorMessage ?? "");
24453
24465
 
24454
24466
  const failedSelector = managedFallback ? controller.currentSelector() : undefined;
@@ -165,6 +165,27 @@ function isRetryableReplacementReceiptCleanupError(error: unknown): boolean {
165
165
  return error instanceof Error && error.message === "managed_replace_receipt_cleanup_pending:io_error";
166
166
  }
167
167
 
168
+ /**
169
+ * A receipt retirement slot is named only from the receipt and predecessor
170
+ * identities, so every process sharing a managed scope — the publisher retiring
171
+ * its own receipt and each peer reconciler that listed it — claims the same
172
+ * slot. Native exact-unlink reports `quarantine_collision` from its no-replace
173
+ * claim before any rename, so a collision that carries no detached or retained
174
+ * path proves only that a peer owns the retirement right now: this process
175
+ * changed nothing, the receipt stays where a later reconciliation finds it, and
176
+ * the unrelated mutation that triggered the scan must not fail.
177
+ */
178
+ function isPeerOwnedReceiptRetirement(result: NativeExactUnlinkResult): boolean {
179
+ return (
180
+ !result.ok &&
181
+ result.code === "quarantine_collision" &&
182
+ result.detachedPath === undefined &&
183
+ result.retainedSuccessorPath === undefined &&
184
+ result.retainedPlaceholderPath === undefined &&
185
+ result.retainedUnknownPath === undefined
186
+ );
187
+ }
188
+
168
189
  /** A same-filesystem rename updates the moved root's ctime but no other tree identity. */
169
190
  function sameDirectoryTreeSnapshotAfterRename(
170
191
  left: NativeDirectoryTreeSnapshot,
@@ -1622,6 +1643,7 @@ export class ManagedSessionDescendantStore {
1622
1643
  sha256: placeholder.identity.sha256,
1623
1644
  quarantineName,
1624
1645
  });
1646
+ if (isPeerOwnedReceiptRetirement(removed)) return true;
1625
1647
  if (
1626
1648
  (!exactUnlinkCompleted(removed) && removed.code !== "not_found") ||
1627
1649
  removed.retainedSuccessorPath !== undefined ||
@@ -1660,6 +1682,7 @@ export class ManagedSessionDescendantStore {
1660
1682
  detachOnly: true,
1661
1683
  quarantineName,
1662
1684
  });
1685
+ if (isPeerOwnedReceiptRetirement(detached)) return;
1663
1686
  if (
1664
1687
  !detached.ok &&
1665
1688
  detached.code === "not_found" &&
package/src/tools/bash.ts CHANGED
@@ -10,10 +10,26 @@ import { type BashArtifactSaveResult, type BashResult, executeBash } from "../ex
10
10
 
11
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
12
  import { buildGjcRuntimeSessionEnv } from "../gjc-runtime/goal-mode-request";
13
+ import {
14
+ MANAGED_OWNER_PREDECESSOR_GENERATION_ENV,
15
+ MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV,
16
+ MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
17
+ MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
18
+ MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
19
+ } from "../gjc-runtime/managed-owner-admission";
20
+ import {
21
+ MANAGED_OWNER_CHILD_TOKEN_ENV,
22
+ MANAGED_OWNER_GENERATION_ENV,
23
+ MANAGED_OWNER_INCARNATION_ENV,
24
+ MANAGED_OWNER_RUN_ID_ENV,
25
+ MANAGED_OWNER_STATE_DIR_ENV,
26
+ } from "../gjc-runtime/managed-owner-supervisor";
13
27
  import {
14
28
  GJC_RALPLAN_ARTIFACT_ENV,
15
29
  GJC_RESTRICTED_ROLE_AGENT_BASH_ENV,
16
30
  } from "../gjc-runtime/restricted-role-agent-bash";
31
+ import { GJC_TMUX_OWNER_SERVER_KEY_ENV } from "../gjc-runtime/session-state-sidecar";
32
+ import { GJC_TMUX_LAUNCHED_ENV } from "../gjc-runtime/windows-powershell-command";
17
33
  import { InternalUrlRouter } from "../internal-urls";
18
34
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
19
35
  import { highlightCode, type Theme } from "../modes/theme/theme";
@@ -94,6 +110,27 @@ const ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
94
110
  const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
95
111
  const MASTER_CAPABILITY_ENV = "GJC_MASTER_CAPABILITY";
96
112
  const MASTER_OWNER_SESSION_ENV = "GJC_MASTER_OWNER_SESSION_ID";
113
+ // Managed-owner env vars that must be scrubbed from child processes.
114
+ // Exported for testing the env scrubbing behavior.
115
+ export const MANAGED_OWNER_BASH_ENV = [
116
+ MANAGED_OWNER_STATE_DIR_ENV,
117
+ MANAGED_OWNER_GENERATION_ENV,
118
+ MANAGED_OWNER_RUN_ID_ENV,
119
+ MANAGED_OWNER_INCARNATION_ENV,
120
+ MANAGED_OWNER_CHILD_TOKEN_ENV,
121
+ MANAGED_OWNER_PREDECESSOR_TOKEN_ENV,
122
+ MANAGED_OWNER_PREDECESSOR_GENERATION_ENV,
123
+ MANAGED_OWNER_PREDECESSOR_RUN_ID_ENV,
124
+ MANAGED_OWNER_PREDECESSOR_INCARNATION_ENV,
125
+ MANAGED_OWNER_TRANSCRIPT_PATH_ENV,
126
+ // The rest of the tmux-owner tuple (tmux-sessions.ts managedEnvironment): leaving
127
+ // either behind makes ownerTerminalContextFromEnvironment() report "invalid" in a
128
+ // nested gjc, because a server key or GJC_TMUX_LAUNCHED=1 without generation/state
129
+ // dir is an incomplete owner context.
130
+ GJC_TMUX_OWNER_SERVER_KEY_ENV,
131
+ GJC_TMUX_LAUNCHED_ENV,
132
+ ] as const;
133
+
97
134
  const COORDINATOR_ONLY_BASH_ENV = [
98
135
  "GJC_COORDINATOR_SESSION_STATE_FILE",
99
136
  "GJC_COORDINATOR_SESSION_ID",
@@ -102,6 +139,8 @@ const COORDINATOR_ONLY_BASH_ENV = [
102
139
  "GJC_COORDINATOR_SESSION_READINESS_FILE",
103
140
  "GJC_COORDINATOR_SIDECAR_SIGNATURE_REQUIRED",
104
141
  "GJC_COORDINATOR_SIDECAR_KEY_ID",
142
+ // Managed-owner env family from managed-owner-supervisor.ts and managed-owner-admission.ts
143
+ ...MANAGED_OWNER_BASH_ENV,
105
144
  ] as const;
106
145
  const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
107
146
  const ACP_RELEASE_TIMEOUT_MS = 1_000;
@@ -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
- }