@gajae-code/coding-agent 0.16.3 → 0.16.4
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 +59 -0
- package/dist/types/cli/args.d.ts +2 -2
- package/dist/types/cli/root-flags.d.ts +4 -2
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/model-resolver.d.ts +9 -0
- package/dist/types/config/settings-schema.d.ts +29 -1
- package/dist/types/discovery/claude-plugins.d.ts +3 -1
- package/dist/types/discovery/helpers.d.ts +2 -0
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +1 -1
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/settings-selector.d.ts +6 -1
- package/dist/types/modes/components/status-line/command.d.ts +35 -0
- package/dist/types/modes/components/status-line/index.d.ts +1 -0
- package/dist/types/modes/components/status-line/types.d.ts +5 -0
- package/dist/types/modes/components/tool-status-header.d.ts +14 -0
- package/dist/types/modes/controllers/event-controller.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/client.d.ts +9 -1
- package/dist/types/runtime-mcp/manager.d.ts +7 -1
- package/dist/types/runtime-mcp/pool.d.ts +22 -3
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/runtime-mcp/types.d.ts +22 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
- package/dist/types/sdk/host/session-runtime.d.ts +23 -0
- package/dist/types/sdk/session-api-key.d.ts +26 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/setup/paseo/announce.d.ts +111 -0
- package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +2 -1
- package/dist/types/tools/fetch.d.ts +7 -0
- package/dist/types/tools/index.d.ts +8 -0
- package/dist/types/tools/path-utils.d.ts +2 -0
- package/dist/types/tools/search.d.ts +6 -0
- package/package.json +7 -7
- package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -0
- package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
- package/src/cli/args.ts +20 -9
- package/src/cli/root-flags.ts +5 -3
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/file-lock.ts +181 -17
- package/src/config/model-preset-registry.ts +5 -3
- package/src/config/model-profiles.ts +40 -0
- package/src/config/model-registry.ts +57 -11
- package/src/config/model-resolver.ts +48 -0
- package/src/config/settings-schema.ts +33 -1
- package/src/config/settings.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
- package/src/discovery/builtin.ts +13 -4
- package/src/discovery/claude-plugins.ts +12 -4
- package/src/discovery/helpers.ts +10 -2
- package/src/extensibility/gjc-plugins/compiler.ts +208 -58
- package/src/extensibility/gjc-plugins/installer.ts +13 -12
- package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
- package/src/extensibility/gjc-plugins/paths.ts +13 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
- package/src/extensibility/runtime-skill-discovery.ts +77 -151
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +44 -16
- package/src/gjc-runtime/repository-binding.ts +1 -1
- package/src/gjc-runtime/session-state-lock.ts +92 -2
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +72 -34
- package/src/modes/DESIGN.md +54 -0
- package/src/modes/components/assistant-message.ts +22 -9
- package/src/modes/components/model-selector.ts +166 -33
- package/src/modes/components/settings-selector.ts +1075 -235
- package/src/modes/components/status-line/command.ts +190 -0
- package/src/modes/components/status-line/index.ts +1 -0
- package/src/modes/components/status-line/segments.ts +32 -5
- package/src/modes/components/status-line/types.ts +5 -0
- package/src/modes/components/tool-status-header.ts +393 -21
- package/src/modes/controllers/command-controller.ts +2 -0
- package/src/modes/controllers/event-controller.ts +148 -5
- package/src/modes/controllers/selector-controller.ts +21 -0
- package/src/modes/interactive-mode.ts +47 -1
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/prompts/tools/search.md +2 -0
- package/src/runtime-mcp/client.ts +60 -17
- package/src/runtime-mcp/manager.ts +171 -14
- package/src/runtime-mcp/pool.test.ts +209 -0
- package/src/runtime-mcp/pool.ts +224 -23
- package/src/runtime-mcp/transports/stdio.ts +158 -24
- package/src/runtime-mcp/types.ts +24 -0
- package/src/sdk/broker/broker.ts +5 -4
- package/src/sdk/bus/chat-daemon-control.ts +1 -1
- package/src/sdk/bus/chat-daemon-runtime.ts +37 -1
- package/src/sdk/bus/slack-daemon.ts +92 -2
- package/src/sdk/bus/slack-live-provider.ts +12 -0
- package/src/sdk/bus/slack-provider.ts +5 -0
- package/src/sdk/cli/session-cli.ts +9 -6
- package/src/sdk/host/session-runtime.test.ts +2 -2
- package/src/sdk/host/session-runtime.ts +275 -43
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/session-api-key.ts +94 -0
- package/src/sdk/session.ts +73 -29
- package/src/sdk/transport/serve-cli.ts +3 -3
- package/src/session/agent-session.ts +123 -38
- package/src/session/contribution-prep.ts +214 -21
- package/src/setup/paseo/announce.ts +450 -0
- package/src/setup/paseo/setup-deps.ts +15 -3
- package/src/slash-commands/builtin-registry.ts +1 -0
- package/src/system-prompt.ts +4 -0
- package/src/task/executor.ts +2 -1
- package/src/task/index.ts +17 -4
- package/src/tools/bash.ts +35 -8
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +29 -1
- package/src/tools/index.ts +8 -0
- package/src/tools/job.ts +8 -2
- package/src/tools/monitor.ts +8 -2
- package/src/tools/path-utils.ts +7 -0
- package/src/tools/search.ts +116 -58
- package/src/tools/skill.ts +22 -2
- package/src/tools/subagent.ts +3 -1
- package/src/tools/tool-catalog.generated.ts +8 -1
- package/src/web/insane/bridge.ts +113 -23
- package/src/web/search/providers/perplexity.ts +102 -41
package/src/tools/cron.ts
CHANGED
|
@@ -592,7 +592,7 @@ export class CronTool implements AgentTool<typeof cronSchema, CronToolDetails> {
|
|
|
592
592
|
}
|
|
593
593
|
|
|
594
594
|
#resolveOwnedJobManager(): AsyncJobManager | undefined {
|
|
595
|
-
const endpointId = this.session.getSessionId?.() ?? undefined;
|
|
595
|
+
const endpointId = this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined;
|
|
596
596
|
return (
|
|
597
597
|
this.session.getAsyncJobManager?.() ?? AsyncJobManager.forEndpoint(endpointId) ?? AsyncJobManager.instance()
|
|
598
598
|
);
|
package/src/tools/fetch.ts
CHANGED
|
@@ -1167,13 +1167,36 @@ interface ReadUrlCacheEntry {
|
|
|
1167
1167
|
wrappedPreambleChars?: number;
|
|
1168
1168
|
}
|
|
1169
1169
|
|
|
1170
|
+
// Entries hold whole-page outputs and image payloads, so this map must stay
|
|
1171
|
+
// bounded in long-lived hosts (TUI, SDK broker). Each fetch caches one entry
|
|
1172
|
+
// under up to two keys (requested + final URL). FIFO eviction is sufficient:
|
|
1173
|
+
// a miss simply refetches, so correctness never depends on a hit.
|
|
1174
|
+
export const READ_URL_CACHE_MAX_KEYS = 64;
|
|
1175
|
+
|
|
1170
1176
|
const readUrlCache = new Map<string, ReadUrlCacheEntry>();
|
|
1171
1177
|
|
|
1172
1178
|
function getReadUrlCacheKey(session: ToolSession, requestedUrl: string, raw: boolean): string {
|
|
1173
|
-
const
|
|
1179
|
+
const sessionFile = session.getSessionFile();
|
|
1180
|
+
const sessionId = session.getSessionId?.();
|
|
1181
|
+
const scope = sessionFile ? `file:${sessionFile}` : sessionId ? `session:${sessionId}` : `cwd:${session.cwd}`;
|
|
1174
1182
|
return `${scope}::${raw ? "raw" : "rendered"}::${normalizeUrl(requestedUrl)}`;
|
|
1175
1183
|
}
|
|
1176
1184
|
|
|
1185
|
+
/** Test-only seam for deterministic module-global cache assertions. */
|
|
1186
|
+
export const readUrlCacheTestHooks = {
|
|
1187
|
+
get size(): number {
|
|
1188
|
+
return readUrlCache.size;
|
|
1189
|
+
},
|
|
1190
|
+
get retainedOutputChars(): number {
|
|
1191
|
+
let total = 0;
|
|
1192
|
+
for (const entry of new Set(readUrlCache.values())) total += entry.output.length;
|
|
1193
|
+
return total;
|
|
1194
|
+
},
|
|
1195
|
+
reset(): void {
|
|
1196
|
+
readUrlCache.clear();
|
|
1197
|
+
},
|
|
1198
|
+
};
|
|
1199
|
+
|
|
1177
1200
|
async function readArtifactOutput(session: ToolSession, artifactId: string): Promise<string | null> {
|
|
1178
1201
|
const artifactsDir = session.getArtifactsDir?.();
|
|
1179
1202
|
if (!artifactsDir) return null;
|
|
@@ -1224,6 +1247,11 @@ async function ensureReadUrlCacheArtifact(session: ToolSession, entry: ReadUrlCa
|
|
|
1224
1247
|
function cacheReadUrlEntry(session: ToolSession, requestedUrl: string, raw: boolean, entry: ReadUrlCacheEntry): void {
|
|
1225
1248
|
readUrlCache.set(getReadUrlCacheKey(session, requestedUrl, raw), entry);
|
|
1226
1249
|
readUrlCache.set(getReadUrlCacheKey(session, entry.details.finalUrl, raw), entry);
|
|
1250
|
+
while (readUrlCache.size > READ_URL_CACHE_MAX_KEYS) {
|
|
1251
|
+
const oldest = readUrlCache.keys().next().value;
|
|
1252
|
+
if (oldest === undefined) break;
|
|
1253
|
+
readUrlCache.delete(oldest);
|
|
1254
|
+
}
|
|
1227
1255
|
}
|
|
1228
1256
|
|
|
1229
1257
|
async function buildReadUrlCacheEntry(
|
package/src/tools/index.ts
CHANGED
|
@@ -249,6 +249,14 @@ export interface ToolSession {
|
|
|
249
249
|
waitForUserSteering?: (signal: AbortSignal) => Promise<void>;
|
|
250
250
|
/** Get session ID */
|
|
251
251
|
getSessionId?: () => string | null;
|
|
252
|
+
/**
|
|
253
|
+
* Opaque async-job ownership/lookup endpoint key. This is not a workflow or
|
|
254
|
+
* path identity: workflow/session consumers must use getSessionId(). Only
|
|
255
|
+
* AsyncJobManager ownership and endpoint-first lookup may use this accessor
|
|
256
|
+
* (subagent-inherited managers are registered under the parent's endpoint;
|
|
257
|
+
* review thread P1).
|
|
258
|
+
*/
|
|
259
|
+
getAsyncEndpointId?: () => string | null;
|
|
252
260
|
/** Get credential-selection session identity. */
|
|
253
261
|
getCredentialSessionId?: () => string | null;
|
|
254
262
|
/** Scope-held MCP facade for mcp:// resolution. */
|
package/src/tools/job.ts
CHANGED
|
@@ -105,7 +105,9 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
105
105
|
): Promise<AgentToolResult<JobToolDetails>> {
|
|
106
106
|
const manager =
|
|
107
107
|
this.session.getAsyncJobManager?.() ??
|
|
108
|
-
AsyncJobManager.forEndpoint(
|
|
108
|
+
AsyncJobManager.forEndpoint(
|
|
109
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
|
|
110
|
+
) ??
|
|
109
111
|
AsyncJobManager.instance();
|
|
110
112
|
if (!manager) {
|
|
111
113
|
return {
|
|
@@ -396,7 +398,11 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
|
396
398
|
const generation = manager.getJob?.(job.id)?.generation;
|
|
397
399
|
if (!generation) continue;
|
|
398
400
|
// The endpoint disambiguates concurrent sessions' same job ids (review P1).
|
|
399
|
-
const registration = lookupOwnedRegistration(
|
|
401
|
+
const registration = lookupOwnedRegistration(
|
|
402
|
+
job.id,
|
|
403
|
+
generation,
|
|
404
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local",
|
|
405
|
+
);
|
|
400
406
|
if (registration) unregisterOwnedRegistration(registration);
|
|
401
407
|
}
|
|
402
408
|
|
package/src/tools/monitor.ts
CHANGED
|
@@ -119,7 +119,9 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
|
|
|
119
119
|
// different concurrent top-level session (review thread P1).
|
|
120
120
|
const manager =
|
|
121
121
|
this.session.getAsyncJobManager?.() ??
|
|
122
|
-
AsyncJobManager.forEndpoint(
|
|
122
|
+
AsyncJobManager.forEndpoint(
|
|
123
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
|
|
124
|
+
) ??
|
|
123
125
|
AsyncJobManager.instance();
|
|
124
126
|
if (!manager) {
|
|
125
127
|
throw new ToolError("Async execution is disabled; the monitor tool is unavailable in this session.");
|
|
@@ -197,7 +199,11 @@ export class MonitorTool implements AgentTool<typeof monitorSchema, MonitorToolD
|
|
|
197
199
|
// endpoint-less fallback scan could attach a foreign session's
|
|
198
200
|
// registration (review thread P2).
|
|
199
201
|
const registration = generation
|
|
200
|
-
? lookupOwnedRegistration(
|
|
202
|
+
? lookupOwnedRegistration(
|
|
203
|
+
jobId,
|
|
204
|
+
generation,
|
|
205
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? "local",
|
|
206
|
+
)
|
|
201
207
|
: undefined;
|
|
202
208
|
const ownedCompletion = registration
|
|
203
209
|
? {
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -696,6 +696,8 @@ export interface ToolScopeOptions {
|
|
|
696
696
|
surfaceExactFilePaths?: boolean;
|
|
697
697
|
/** Extra hint appended to "Path not found" when stat fails and the user supplied multiple paths. */
|
|
698
698
|
multipathStatHint?: string;
|
|
699
|
+
/** Cancels internal-URL resolution and prevents further scope work. */
|
|
700
|
+
signal?: AbortSignal;
|
|
699
701
|
}
|
|
700
702
|
|
|
701
703
|
export interface ToolScopeResolution {
|
|
@@ -719,6 +721,7 @@ export interface ToolScopeResolution {
|
|
|
719
721
|
*/
|
|
720
722
|
export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<ToolScopeResolution> {
|
|
721
723
|
const { rawPaths: inputs, cwd, internalUrlAction, getArtifactsDir, getAuthorizedArtifactsDirs } = opts;
|
|
724
|
+
opts.signal?.throwIfAborted();
|
|
722
725
|
const rawPaths = inputs.map(normalizePathLikeInput);
|
|
723
726
|
if (rawPaths.some(rawPath => rawPath.length === 0)) {
|
|
724
727
|
throw new ToolError("`paths` must contain non-empty paths or globs");
|
|
@@ -727,6 +730,7 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
727
730
|
const resolvedPathInputs: string[] = [];
|
|
728
731
|
const immutableSourcePaths = new Set<string>();
|
|
729
732
|
for (const rawPath of rawPaths) {
|
|
733
|
+
opts.signal?.throwIfAborted();
|
|
730
734
|
if (!internalRouter.canHandle(rawPath)) {
|
|
731
735
|
resolvedPathInputs.push(rawPath);
|
|
732
736
|
continue;
|
|
@@ -739,7 +743,9 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
739
743
|
getArtifactsDir,
|
|
740
744
|
getAuthorizedArtifactsDirs,
|
|
741
745
|
mcpManager: opts.mcpManager,
|
|
746
|
+
signal: opts.signal,
|
|
742
747
|
});
|
|
748
|
+
opts.signal?.throwIfAborted();
|
|
743
749
|
if (!resource.sourcePath) {
|
|
744
750
|
throw new ToolError(`Cannot ${internalUrlAction} internal URL without a backing file: ${rawPath}`);
|
|
745
751
|
}
|
|
@@ -753,6 +759,7 @@ export async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<To
|
|
|
753
759
|
let effectivePaths = resolvedPathInputs;
|
|
754
760
|
if (resolvedPathInputs.length > 1) {
|
|
755
761
|
const partition = await partitionExistingPaths(resolvedPathInputs, cwd, parseSearchPath);
|
|
762
|
+
opts.signal?.throwIfAborted();
|
|
756
763
|
if (partition.valid.length === 0) {
|
|
757
764
|
throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
758
765
|
}
|
package/src/tools/search.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallb
|
|
|
5
5
|
import type { GrepMatch, GrepResult, grep as grepFn } from "@gajae-code/natives";
|
|
6
6
|
import type { Component } from "@gajae-code/tui";
|
|
7
7
|
import { Text } from "@gajae-code/tui";
|
|
8
|
-
import { prompt
|
|
8
|
+
import { prompt } from "@gajae-code/utils";
|
|
9
9
|
import * as z from "zod/v4";
|
|
10
10
|
import { getFileReadCache } from "../edit/file-read-cache";
|
|
11
11
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
PREVIEW_LIMITS,
|
|
36
36
|
splitGroupsByBlankLine,
|
|
37
37
|
} from "./render-utils";
|
|
38
|
-
import { ToolError } from "./tool-errors";
|
|
38
|
+
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
39
39
|
import { toolResult } from "./tool-result";
|
|
40
40
|
|
|
41
41
|
let searchNativesLoad:
|
|
@@ -71,6 +71,7 @@ const searchSchema = z
|
|
|
71
71
|
.number()
|
|
72
72
|
.optional()
|
|
73
73
|
.describe("files to skip before collecting results — use to paginate when the prior call hit the file limit"),
|
|
74
|
+
timeout: z.number().min(0.5).max(60).default(5).describe("timeout in seconds (0.5–60)").optional(),
|
|
74
75
|
})
|
|
75
76
|
.strict();
|
|
76
77
|
|
|
@@ -90,6 +91,9 @@ export const SINGLE_FILE_MATCHES = 200;
|
|
|
90
91
|
* (DEFAULT_FILE_LIMIT files × MULTI_FILE_PER_FILE_MATCHES matches) plus
|
|
91
92
|
* pagination headroom so the caller can see total file count. */
|
|
92
93
|
const INTERNAL_TOTAL_CAP = 2000;
|
|
94
|
+
const DEFAULT_SEARCH_TIMEOUT_MS = 5000;
|
|
95
|
+
const MIN_SEARCH_TIMEOUT_MS = 500;
|
|
96
|
+
const MAX_SEARCH_TIMEOUT_MS = 60_000;
|
|
93
97
|
|
|
94
98
|
/**
|
|
95
99
|
* Detect a `,` that is not inside a `{…}` brace expansion. Used to catch
|
|
@@ -130,6 +134,7 @@ function containsTopLevelComma(entry: string): boolean {
|
|
|
130
134
|
async function resolveArchiveSearchPaths(
|
|
131
135
|
paths: string[],
|
|
132
136
|
cwd: string,
|
|
137
|
+
signal?: AbortSignal,
|
|
133
138
|
): Promise<{
|
|
134
139
|
resolvedPaths: string[];
|
|
135
140
|
displayMap: Map<string, string>;
|
|
@@ -143,64 +148,71 @@ async function resolveArchiveSearchPaths(
|
|
|
143
148
|
const unreadable: string[] = [];
|
|
144
149
|
let tempDir: string | undefined;
|
|
145
150
|
const archiveCache = new Map<string, ArchiveReader>();
|
|
151
|
+
const cleanup = async () => {
|
|
152
|
+
const directory = tempDir;
|
|
153
|
+
tempDir = undefined;
|
|
154
|
+
if (directory) await rm(directory, { recursive: true, force: true }).catch(() => {});
|
|
155
|
+
};
|
|
146
156
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
try {
|
|
158
|
+
for (let idx = 0; idx < paths.length; idx++) {
|
|
159
|
+
signal?.throwIfAborted();
|
|
160
|
+
const entry = paths[idx];
|
|
161
|
+
const candidates = parseArchivePathCandidates(entry);
|
|
162
|
+
// Longest archive prefix first; we want the one whose member portion is non-empty.
|
|
163
|
+
const member = candidates.find(c => c.subPath !== "" && c.archivePath !== entry);
|
|
164
|
+
if (!member) continue;
|
|
165
|
+
|
|
166
|
+
const archiveAbs = resolveReadPath(member.archivePath, cwd);
|
|
167
|
+
let archive = archiveCache.get(archiveAbs);
|
|
168
|
+
if (!archive) {
|
|
169
|
+
try {
|
|
170
|
+
archive = await openArchive(archiveAbs);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
unreadable.push(`${entry} (cannot open archive: ${(err as Error).message})`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
archiveCache.set(archiveAbs, archive);
|
|
176
|
+
}
|
|
153
177
|
|
|
154
|
-
|
|
155
|
-
let archive = archiveCache.get(archiveAbs);
|
|
156
|
-
if (!archive) {
|
|
178
|
+
let extracted: ExtractedArchiveFile;
|
|
157
179
|
try {
|
|
158
|
-
|
|
180
|
+
extracted = await archive.readFile(member.subPath);
|
|
159
181
|
} catch (err) {
|
|
160
|
-
unreadable.push(`${entry} (
|
|
182
|
+
unreadable.push(`${entry} (${(err as Error).message})`);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
signal?.throwIfAborted();
|
|
186
|
+
// UTF-8 only — binary members would just produce noise through ripgrep.
|
|
187
|
+
if (extracted.bytes.some(byte => byte === 0)) {
|
|
188
|
+
unreadable.push(`${entry} (binary archive entry)`);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
let text: string;
|
|
192
|
+
try {
|
|
193
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(extracted.bytes);
|
|
194
|
+
} catch {
|
|
195
|
+
unreadable.push(`${entry} (non-UTF-8 archive entry)`);
|
|
161
196
|
continue;
|
|
162
197
|
}
|
|
163
|
-
archiveCache.set(archiveAbs, archive);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
let extracted: ExtractedArchiveFile;
|
|
167
|
-
try {
|
|
168
|
-
extracted = await archive.readFile(member.subPath);
|
|
169
|
-
} catch (err) {
|
|
170
|
-
unreadable.push(`${entry} (${(err as Error).message})`);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
// UTF-8 only — binary members would just produce noise through ripgrep.
|
|
174
|
-
if (extracted.bytes.some(byte => byte === 0)) {
|
|
175
|
-
unreadable.push(`${entry} (binary archive entry)`);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
let text: string;
|
|
179
|
-
try {
|
|
180
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(extracted.bytes);
|
|
181
|
-
} catch {
|
|
182
|
-
unreadable.push(`${entry} (non-UTF-8 archive entry)`);
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
198
|
|
|
186
|
-
|
|
187
|
-
|
|
199
|
+
if (!tempDir) {
|
|
200
|
+
tempDir = await mkdtemp(path.join(tmpdir(), "gjc-search-archive-"));
|
|
201
|
+
}
|
|
202
|
+
// Per-entry filename keeps the scratch path unique even when two selectors
|
|
203
|
+
// resolve to members with the same basename.
|
|
204
|
+
const safeBase = path.basename(member.subPath).replace(/[^\w.-]+/g, "_") || "entry";
|
|
205
|
+
const tempPath = path.join(tempDir, `${idx}-${safeBase}`);
|
|
206
|
+
await writeFile(tempPath, text);
|
|
207
|
+
resolvedPaths[idx] = tempPath;
|
|
208
|
+
displayMap.set(tempPath, entry);
|
|
209
|
+
displaySet.add(entry);
|
|
188
210
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const tempPath = path.join(tempDir, `${idx}-${safeBase}`);
|
|
193
|
-
await writeFile(tempPath, text);
|
|
194
|
-
resolvedPaths[idx] = tempPath;
|
|
195
|
-
displayMap.set(tempPath, entry);
|
|
196
|
-
displaySet.add(entry);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
await cleanup();
|
|
213
|
+
throw error;
|
|
197
214
|
}
|
|
198
215
|
|
|
199
|
-
const cleanup = async () => {
|
|
200
|
-
if (tempDir) {
|
|
201
|
-
await rm(tempDir, { recursive: true, force: true }).catch(() => {});
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
216
|
return { resolvedPaths, displayMap, displaySet, unreadable, cleanup };
|
|
205
217
|
}
|
|
206
218
|
|
|
@@ -235,6 +247,8 @@ export interface SearchToolOptions {
|
|
|
235
247
|
context?: number;
|
|
236
248
|
/** Per-call total match cap used by Cursor's native Pi grep frame. */
|
|
237
249
|
totalMatchLimit?: number;
|
|
250
|
+
/** Test or remote override for the native grep implementation. */
|
|
251
|
+
grep?: typeof grepFn;
|
|
238
252
|
}
|
|
239
253
|
|
|
240
254
|
type SearchParams = z.infer<typeof searchSchema>;
|
|
@@ -266,12 +280,23 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
266
280
|
_onUpdate?: AgentToolUpdateCallback<SearchToolDetails>,
|
|
267
281
|
_toolContext?: AgentToolContext,
|
|
268
282
|
): Promise<AgentToolResult<SearchToolDetails>> {
|
|
269
|
-
const { pattern, i, gitignore, skip } = params;
|
|
283
|
+
const { pattern, i, gitignore, skip, timeout } = params;
|
|
270
284
|
// Recurring real-world failure mode: models omit `paths` for repo-wide
|
|
271
285
|
// searches. Default to the working directory instead of failing the call.
|
|
272
286
|
const paths = params.paths ?? ["."];
|
|
287
|
+
const requestedTimeoutMs = timeout != null ? Math.round(timeout * 1000) : DEFAULT_SEARCH_TIMEOUT_MS;
|
|
288
|
+
const timeoutMs = Math.min(MAX_SEARCH_TIMEOUT_MS, Math.max(MIN_SEARCH_TIMEOUT_MS, requestedTimeoutMs));
|
|
289
|
+
const deadlineAt = Date.now() + timeoutMs;
|
|
290
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
291
|
+
const combinedSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
292
|
+
const throwIfSearchCancelled = (): void => {
|
|
293
|
+
signal?.throwIfAborted();
|
|
294
|
+
if (Date.now() >= deadlineAt) throw new Error("Aborted: Timeout");
|
|
295
|
+
combinedSignal.throwIfAborted();
|
|
296
|
+
};
|
|
273
297
|
|
|
274
|
-
|
|
298
|
+
try {
|
|
299
|
+
throwIfSearchCancelled();
|
|
275
300
|
const normalizedPattern = pattern.trim();
|
|
276
301
|
if (!normalizedPattern) {
|
|
277
302
|
throw new ToolError("Pattern must not be empty");
|
|
@@ -292,8 +317,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
292
317
|
displaySet: archiveDisplaySet,
|
|
293
318
|
unreadable: archiveUnreadable,
|
|
294
319
|
cleanup: cleanupArchiveScratch,
|
|
295
|
-
} = await resolveArchiveSearchPaths(paths, this.session.cwd);
|
|
320
|
+
} = await resolveArchiveSearchPaths(paths, this.session.cwd, combinedSignal);
|
|
321
|
+
let completedResult: AgentToolResult<SearchToolDetails> | undefined;
|
|
296
322
|
try {
|
|
323
|
+
throwIfSearchCancelled();
|
|
297
324
|
if (archiveUnreadable.length > 0 && resolvedPaths.length === archiveUnreadable.length) {
|
|
298
325
|
// All inputs were archive selectors we couldn't materialize; surface the
|
|
299
326
|
// reason instead of a downstream "path not found" from the scope resolver.
|
|
@@ -324,7 +351,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
324
351
|
trackImmutableSources: true,
|
|
325
352
|
surfaceExactFilePaths: true,
|
|
326
353
|
multipathStatHint: " (`paths` entries must each exist relative to cwd)",
|
|
354
|
+
signal: combinedSignal,
|
|
327
355
|
});
|
|
356
|
+
throwIfSearchCancelled();
|
|
328
357
|
const { searchPath, isDirectory, multiTargets, exactFilePaths, missingPaths, immutableSourcePaths } = scope;
|
|
329
358
|
// When the only input was an archive selector, surface that selector instead
|
|
330
359
|
// of the temp scratch path the resolver substituted in.
|
|
@@ -345,7 +374,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
345
374
|
const baseDisplayMode = resolveFileDisplayMode(this.session);
|
|
346
375
|
const immutableDisplayMode = resolveFileDisplayMode(this.session, { immutable: true });
|
|
347
376
|
|
|
348
|
-
const { GrepOutputMode, grep } = await searchNatives();
|
|
377
|
+
const { GrepOutputMode, grep: nativeGrep } = await searchNatives();
|
|
378
|
+
const grep = this.options.grep ?? nativeGrep;
|
|
349
379
|
const effectiveOutputMode = GrepOutputMode.Content;
|
|
350
380
|
// Multi-scope = more than one file may match. We fetch up to
|
|
351
381
|
// INTERNAL_TOTAL_CAP matches from native grep, then in JS group by
|
|
@@ -363,7 +393,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
363
393
|
let totalMatches = 0;
|
|
364
394
|
let filesSearched = 0;
|
|
365
395
|
const targets = exactFilePaths
|
|
366
|
-
? exactFilePaths.map(filePath => ({
|
|
396
|
+
? exactFilePaths.map(filePath => ({
|
|
397
|
+
basePath: filePath,
|
|
398
|
+
glob: undefined as string | undefined,
|
|
399
|
+
}))
|
|
367
400
|
: (multiTargets ?? []);
|
|
368
401
|
for (const target of targets) {
|
|
369
402
|
const targetResult = await grep(
|
|
@@ -381,9 +414,12 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
381
414
|
contextAfter: normalizedContextAfter,
|
|
382
415
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
383
416
|
mode: effectiveOutputMode,
|
|
417
|
+
signal: combinedSignal,
|
|
418
|
+
timeoutMs,
|
|
384
419
|
},
|
|
385
420
|
undefined,
|
|
386
421
|
);
|
|
422
|
+
throwIfSearchCancelled();
|
|
387
423
|
limitReached = limitReached || Boolean(targetResult.limitReached);
|
|
388
424
|
totalMatches += targetResult.totalMatches;
|
|
389
425
|
filesSearched += targetResult.filesSearched;
|
|
@@ -416,9 +452,12 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
416
452
|
contextAfter: normalizedContextAfter,
|
|
417
453
|
maxColumns: DEFAULT_MAX_COLUMN,
|
|
418
454
|
mode: effectiveOutputMode,
|
|
455
|
+
signal: combinedSignal,
|
|
456
|
+
timeoutMs,
|
|
419
457
|
},
|
|
420
458
|
undefined,
|
|
421
459
|
);
|
|
460
|
+
throwIfSearchCancelled();
|
|
422
461
|
}
|
|
423
462
|
} catch (err) {
|
|
424
463
|
if (err instanceof Error && err.message.startsWith("regex parse error")) {
|
|
@@ -518,7 +557,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
518
557
|
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
519
558
|
};
|
|
520
559
|
const text = warningNote ? `No matches found\n${warningNote}` : "No matches found";
|
|
521
|
-
|
|
560
|
+
const noMatchResult = toolResult(details).text(text).done();
|
|
561
|
+
await cleanupArchiveScratch();
|
|
562
|
+
throwIfSearchCancelled();
|
|
563
|
+
return noMatchResult;
|
|
522
564
|
}
|
|
523
565
|
const outputLines: string[] = [];
|
|
524
566
|
let linesTruncated = false;
|
|
@@ -644,11 +686,25 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
644
686
|
if (truncation.truncated) {
|
|
645
687
|
resultBuilder.truncation(truncation, { direction: "head" });
|
|
646
688
|
}
|
|
647
|
-
|
|
689
|
+
throwIfSearchCancelled();
|
|
690
|
+
completedResult = resultBuilder.done();
|
|
648
691
|
} finally {
|
|
649
692
|
await cleanupArchiveScratch();
|
|
650
693
|
}
|
|
651
|
-
|
|
694
|
+
throwIfSearchCancelled();
|
|
695
|
+
if (!completedResult) throw new Error("Search result assembly did not complete");
|
|
696
|
+
return completedResult;
|
|
697
|
+
} catch (error) {
|
|
698
|
+
const nativeTimedOut = error instanceof Error && error.message === "Aborted: Timeout";
|
|
699
|
+
if ((timeoutSignal.aborted || nativeTimedOut) && !signal?.aborted) {
|
|
700
|
+
const seconds = timeoutMs % 1000 === 0 ? `${timeoutMs / 1000}` : (timeoutMs / 1000).toFixed(1);
|
|
701
|
+
throw new ToolError(`Search timed out after ${seconds}s; increase timeout or narrow paths/pattern`);
|
|
702
|
+
}
|
|
703
|
+
if (combinedSignal.aborted || (error instanceof Error && error.name === "AbortError")) {
|
|
704
|
+
throw new ToolAbortError();
|
|
705
|
+
}
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
652
708
|
}
|
|
653
709
|
}
|
|
654
710
|
|
|
@@ -662,6 +718,7 @@ interface SearchRenderArgs {
|
|
|
662
718
|
i?: boolean;
|
|
663
719
|
gitignore?: boolean;
|
|
664
720
|
skip?: number;
|
|
721
|
+
timeout?: number;
|
|
665
722
|
}
|
|
666
723
|
|
|
667
724
|
const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
@@ -674,6 +731,7 @@ export const searchToolRenderer = {
|
|
|
674
731
|
if (args.i) meta.push("case:insensitive");
|
|
675
732
|
if (args.gitignore === false) meta.push("gitignore:false");
|
|
676
733
|
if (args.skip !== undefined && args.skip > 0) meta.push(`skip:${args.skip}`);
|
|
734
|
+
if (args.timeout !== undefined && args.timeout !== 5) meta.push(`timeout:${args.timeout}s`);
|
|
677
735
|
|
|
678
736
|
const text = renderStatusLine(
|
|
679
737
|
{ icon: "pending", title: "Search", description: args.pattern || "?", meta },
|
package/src/tools/skill.ts
CHANGED
|
@@ -66,6 +66,26 @@ function normalizeSkillName(name: string | undefined): string {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
const SKILL_NAME_GLOB_PATTERN = /[*?[\]{}]/;
|
|
69
|
+
const MAX_AVAILABLE_SKILL_NAMES = 50;
|
|
70
|
+
const MAX_AVAILABLE_SKILL_CHARS = 2048;
|
|
71
|
+
|
|
72
|
+
function formatAvailableSkills(skills: readonly { name: string }[]): string {
|
|
73
|
+
const names = [...new Set(skills.map(skill => skill.name))].sort();
|
|
74
|
+
const visible: string[] = [];
|
|
75
|
+
let visibleChars = 0;
|
|
76
|
+
for (let index = 0; index < names.length && visible.length < MAX_AVAILABLE_SKILL_NAMES; index++) {
|
|
77
|
+
const name = names[index];
|
|
78
|
+
const separatorChars = visible.length > 0 ? 2 : 0;
|
|
79
|
+
const omittedAfterAdding = names.length - (visible.length + 1);
|
|
80
|
+
const markerChars = omittedAfterAdding > 0 ? `, … (+${omittedAfterAdding} more)`.length : 0;
|
|
81
|
+
if (visibleChars + separatorChars + name.length + markerChars > MAX_AVAILABLE_SKILL_CHARS) break;
|
|
82
|
+
visible.push(name);
|
|
83
|
+
visibleChars += separatorChars + name.length;
|
|
84
|
+
}
|
|
85
|
+
const omitted = names.length - visible.length;
|
|
86
|
+
const marker = omitted > 0 ? `… (+${omitted} more)` : "";
|
|
87
|
+
return visible.length > 0 && marker ? `${visible.join(", ")}, ${marker}` : visible.join(", ") || marker;
|
|
88
|
+
}
|
|
69
89
|
|
|
70
90
|
type SkillToolInput = z.infer<typeof skillSchema>;
|
|
71
91
|
|
|
@@ -154,10 +174,10 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
|
|
|
154
174
|
this.#session.home,
|
|
155
175
|
));
|
|
156
176
|
if (!skill) {
|
|
157
|
-
const available = skills
|
|
177
|
+
const available = formatAvailableSkills(skills);
|
|
158
178
|
const hint =
|
|
159
179
|
available.length > 0
|
|
160
|
-
? ` Available: ${available
|
|
180
|
+
? ` Available: ${available}. Use skill_discovery to find project/user runtime skills.`
|
|
161
181
|
: " Use skill_discovery to find project/user runtime skills.";
|
|
162
182
|
throw new ToolError(`skill tool: unknown skill "${requestedName}".${hint}`);
|
|
163
183
|
}
|
package/src/tools/subagent.ts
CHANGED
|
@@ -227,7 +227,9 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
|
|
|
227
227
|
// or belong to a same-id subagent of another session (review thread P1).
|
|
228
228
|
const manager =
|
|
229
229
|
this.session.getAsyncJobManager?.() ??
|
|
230
|
-
AsyncJobManager.forEndpoint(
|
|
230
|
+
AsyncJobManager.forEndpoint(
|
|
231
|
+
this.session.getAsyncEndpointId?.() ?? this.session.getSessionId?.() ?? undefined,
|
|
232
|
+
) ??
|
|
231
233
|
AsyncJobManager.instance();
|
|
232
234
|
if (!manager) {
|
|
233
235
|
return {
|
|
@@ -988,7 +988,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
988
988
|
"search": {
|
|
989
989
|
"name": "search",
|
|
990
990
|
"label": "Search",
|
|
991
|
-
"description": "Searches files using powerful regex matching.\n\n<instruction>\n- Supports Rust regex syntax (RE2-style — no lookaround or backreferences). Use line anchors or post-filters instead of (?!…)/(?<!…)\n- `paths` accepts an array of files, directories, globs, or internal URLs; when omitted, the whole working directory is searched\n- `paths` is an array; do not embed commas or spaces inside a single entry. Pass `[\"src\", \"tests\"]` not `[\"src,tests\"]`.\n- Cross-line patterns are detected from literal `\\n` or escaped `\\\\n` in `pattern`\n</instruction>\n\n<output>\n</output>\n\n<critical>\n- Search paths are an array; pass separate entries rather than comma-joined paths.\n- Use a cross-line pattern only when the match actually spans lines.\n</critical>",
|
|
991
|
+
"description": "Searches files using powerful regex matching.\n\n<instruction>\n- Supports Rust regex syntax (RE2-style — no lookaround or backreferences). Use line anchors or post-filters instead of (?!…)/(?<!…)\n- `paths` accepts an array of files, directories, globs, or internal URLs; when omitted, the whole working directory is searched\n- `paths` is an array; do not embed commas or spaces inside a single entry. Pass `[\"src\", \"tests\"]` not `[\"src,tests\"]`.\n- Cross-line patterns are detected from literal `\\n` or escaped `\\\\n` in `pattern`\n- `timeout` is in seconds (default 5, range 0.5–60). Increase it only for intentionally broad searches; narrow `paths` first\n</instruction>\n\n<output>\n</output>\n\n<critical>\n- Search paths are an array; pass separate entries rather than comma-joined paths.\n- Use a cross-line pattern only when the match actually spans lines.\n- Do not retry a timed-out search unchanged; narrow `paths`/`pattern` or explicitly increase `timeout`.\n</critical>",
|
|
992
992
|
"parameters": {
|
|
993
993
|
"type": "object",
|
|
994
994
|
"properties": {
|
|
@@ -1016,6 +1016,13 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
|
|
|
1016
1016
|
"skip": {
|
|
1017
1017
|
"description": "files to skip before collecting results — use to paginate when the prior call hit the file limit",
|
|
1018
1018
|
"type": "number"
|
|
1019
|
+
},
|
|
1020
|
+
"timeout": {
|
|
1021
|
+
"default": 5,
|
|
1022
|
+
"description": "timeout in seconds (0.5–60)",
|
|
1023
|
+
"type": "number",
|
|
1024
|
+
"minimum": 0.5,
|
|
1025
|
+
"maximum": 60
|
|
1019
1026
|
}
|
|
1020
1027
|
},
|
|
1021
1028
|
"required": [
|