@gajae-code/coding-agent 0.2.3 → 0.2.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.
- package/CHANGELOG.md +34 -8600
- package/README.md +1 -1
- package/dist/types/async/job-manager.d.ts +61 -0
- package/dist/types/cli/update-cli.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +27 -3
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/defaults/gjc-defaults.d.ts +19 -6
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/exec/bash-executor.d.ts +8 -1
- package/dist/types/gjc-runtime/restricted-role-agent-bash.d.ts +2 -0
- package/dist/types/modes/acp/acp-client-bridge.d.ts +1 -1
- package/dist/types/modes/components/settings-selector.d.ts +4 -0
- package/dist/types/modes/components/skill-hud/render.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/theme/defaults/index.d.ts +45 -9351
- package/dist/types/modes/theme/theme.d.ts +6 -5
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/streaming-output.d.ts +11 -0
- package/dist/types/skill-state/active-state.d.ts +1 -0
- package/dist/types/task/types.d.ts +1 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +5 -0
- package/dist/types/tools/bash.d.ts +24 -0
- package/dist/types/tools/cron.d.ts +110 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/monitor.d.ts +54 -0
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/provider.d.ts +11 -4
- package/dist/types/web/search/providers/duckduckgo.d.ts +57 -0
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +7 -7
- package/src/async/job-manager.ts +224 -0
- package/src/cli/agents-cli.ts +3 -0
- package/src/cli/update-cli.ts +67 -16
- package/src/config/settings-schema.ts +30 -2
- package/src/config/settings.ts +44 -7
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +48 -6
- package/src/defaults/gjc/skills/deep-interview/auto-answer-uncertain.md +37 -0
- package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +42 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +8 -4
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +9 -6
- package/src/defaults/gjc-defaults.ts +68 -16
- package/src/discovery/helpers.ts +5 -0
- package/src/eval/js/shared/rewrite-imports.ts +1 -2
- package/src/exec/bash-executor.ts +20 -9
- package/src/gjc-runtime/deep-interview-runtime.ts +44 -0
- package/src/gjc-runtime/ralplan-runtime.ts +2 -0
- package/src/gjc-runtime/restricted-role-agent-bash.ts +5 -0
- package/src/gjc-runtime/state-runtime.ts +3 -2
- package/src/goals/tools/goal-tool.ts +5 -1
- package/src/hooks/skill-state.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +8 -4
- package/src/lsp/render.ts +1 -1
- package/src/memories/index.ts +5 -4
- package/src/modes/acp/acp-agent.ts +1 -1
- package/src/modes/acp/acp-client-bridge.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +1 -1
- package/src/modes/components/diff.ts +2 -2
- package/src/modes/components/settings-selector.ts +25 -14
- package/src/modes/components/skill-hud/render.ts +7 -2
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +10 -2
- package/src/modes/controllers/selector-controller.ts +67 -0
- package/src/modes/interactive-mode.ts +34 -3
- package/src/modes/theme/defaults/blue-crab.json +126 -0
- package/src/modes/theme/defaults/index.ts +2 -196
- package/src/modes/theme/theme.ts +75 -36
- package/src/modes/types.ts +2 -0
- package/src/prompts/agents/architect.md +5 -1
- package/src/prompts/agents/critic.md +5 -1
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/agents/planner.md +5 -1
- package/src/prompts/memories/unavailable.md +9 -0
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/cron.md +25 -0
- package/src/prompts/tools/monitor.md +30 -0
- package/src/runtime-mcp/oauth-flow.ts +4 -2
- package/src/sdk.ts +7 -0
- package/src/session/agent-session.ts +16 -5
- package/src/session/streaming-output.ts +21 -0
- package/src/skill-state/active-state.ts +163 -12
- package/src/slash-commands/builtin-registry.ts +11 -1
- package/src/task/agents.ts +1 -0
- package/src/task/executor.ts +1 -0
- package/src/task/types.ts +1 -0
- package/src/tools/bash-allowed-prefixes.ts +169 -0
- package/src/tools/bash.ts +190 -29
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/cron.ts +665 -0
- package/src/tools/index.ts +20 -2
- package/src/tools/monitor.ts +136 -0
- package/src/vim/engine.ts +3 -3
- package/src/web/search/index.ts +31 -18
- package/src/web/search/provider.ts +57 -12
- package/src/web/search/providers/duckduckgo.ts +279 -0
- package/src/web/search/types.ts +2 -0
- package/src/modes/theme/dark.json +0 -95
- package/src/modes/theme/defaults/alabaster.json +0 -93
- package/src/modes/theme/defaults/amethyst.json +0 -96
- package/src/modes/theme/defaults/anthracite.json +0 -93
- package/src/modes/theme/defaults/basalt.json +0 -91
- package/src/modes/theme/defaults/birch.json +0 -95
- package/src/modes/theme/defaults/dark-abyss.json +0 -91
- package/src/modes/theme/defaults/dark-arctic.json +0 -104
- package/src/modes/theme/defaults/dark-aurora.json +0 -95
- package/src/modes/theme/defaults/dark-catppuccin.json +0 -107
- package/src/modes/theme/defaults/dark-cavern.json +0 -91
- package/src/modes/theme/defaults/dark-copper.json +0 -95
- package/src/modes/theme/defaults/dark-cosmos.json +0 -90
- package/src/modes/theme/defaults/dark-cyberpunk.json +0 -102
- package/src/modes/theme/defaults/dark-dracula.json +0 -98
- package/src/modes/theme/defaults/dark-eclipse.json +0 -91
- package/src/modes/theme/defaults/dark-ember.json +0 -95
- package/src/modes/theme/defaults/dark-equinox.json +0 -90
- package/src/modes/theme/defaults/dark-forest.json +0 -96
- package/src/modes/theme/defaults/dark-github.json +0 -105
- package/src/modes/theme/defaults/dark-gruvbox.json +0 -112
- package/src/modes/theme/defaults/dark-lavender.json +0 -95
- package/src/modes/theme/defaults/dark-lunar.json +0 -89
- package/src/modes/theme/defaults/dark-midnight.json +0 -95
- package/src/modes/theme/defaults/dark-monochrome.json +0 -94
- package/src/modes/theme/defaults/dark-monokai.json +0 -98
- package/src/modes/theme/defaults/dark-nebula.json +0 -90
- package/src/modes/theme/defaults/dark-nord.json +0 -97
- package/src/modes/theme/defaults/dark-ocean.json +0 -101
- package/src/modes/theme/defaults/dark-one.json +0 -100
- package/src/modes/theme/defaults/dark-poimandres.json +0 -141
- package/src/modes/theme/defaults/dark-rainforest.json +0 -91
- package/src/modes/theme/defaults/dark-reef.json +0 -91
- package/src/modes/theme/defaults/dark-retro.json +0 -92
- package/src/modes/theme/defaults/dark-rose-pine.json +0 -96
- package/src/modes/theme/defaults/dark-sakura.json +0 -95
- package/src/modes/theme/defaults/dark-slate.json +0 -95
- package/src/modes/theme/defaults/dark-solarized.json +0 -97
- package/src/modes/theme/defaults/dark-solstice.json +0 -90
- package/src/modes/theme/defaults/dark-starfall.json +0 -91
- package/src/modes/theme/defaults/dark-sunset.json +0 -99
- package/src/modes/theme/defaults/dark-swamp.json +0 -90
- package/src/modes/theme/defaults/dark-synthwave.json +0 -103
- package/src/modes/theme/defaults/dark-taiga.json +0 -91
- package/src/modes/theme/defaults/dark-terminal.json +0 -95
- package/src/modes/theme/defaults/dark-tokyo-night.json +0 -101
- package/src/modes/theme/defaults/dark-tundra.json +0 -91
- package/src/modes/theme/defaults/dark-twilight.json +0 -91
- package/src/modes/theme/defaults/dark-volcanic.json +0 -91
- package/src/modes/theme/defaults/graphite.json +0 -92
- package/src/modes/theme/defaults/light-arctic.json +0 -107
- package/src/modes/theme/defaults/light-aurora-day.json +0 -91
- package/src/modes/theme/defaults/light-canyon.json +0 -91
- package/src/modes/theme/defaults/light-catppuccin.json +0 -106
- package/src/modes/theme/defaults/light-cirrus.json +0 -90
- package/src/modes/theme/defaults/light-coral.json +0 -95
- package/src/modes/theme/defaults/light-cyberpunk.json +0 -96
- package/src/modes/theme/defaults/light-dawn.json +0 -90
- package/src/modes/theme/defaults/light-dunes.json +0 -91
- package/src/modes/theme/defaults/light-eucalyptus.json +0 -95
- package/src/modes/theme/defaults/light-forest.json +0 -100
- package/src/modes/theme/defaults/light-frost.json +0 -95
- package/src/modes/theme/defaults/light-github.json +0 -115
- package/src/modes/theme/defaults/light-glacier.json +0 -91
- package/src/modes/theme/defaults/light-gruvbox.json +0 -108
- package/src/modes/theme/defaults/light-haze.json +0 -90
- package/src/modes/theme/defaults/light-honeycomb.json +0 -95
- package/src/modes/theme/defaults/light-lagoon.json +0 -91
- package/src/modes/theme/defaults/light-lavender.json +0 -95
- package/src/modes/theme/defaults/light-meadow.json +0 -91
- package/src/modes/theme/defaults/light-mint.json +0 -95
- package/src/modes/theme/defaults/light-monochrome.json +0 -101
- package/src/modes/theme/defaults/light-ocean.json +0 -99
- package/src/modes/theme/defaults/light-one.json +0 -99
- package/src/modes/theme/defaults/light-opal.json +0 -91
- package/src/modes/theme/defaults/light-orchard.json +0 -91
- package/src/modes/theme/defaults/light-paper.json +0 -95
- package/src/modes/theme/defaults/light-poimandres.json +0 -141
- package/src/modes/theme/defaults/light-prism.json +0 -90
- package/src/modes/theme/defaults/light-retro.json +0 -98
- package/src/modes/theme/defaults/light-sand.json +0 -95
- package/src/modes/theme/defaults/light-savanna.json +0 -91
- package/src/modes/theme/defaults/light-solarized.json +0 -102
- package/src/modes/theme/defaults/light-soleil.json +0 -90
- package/src/modes/theme/defaults/light-sunset.json +0 -99
- package/src/modes/theme/defaults/light-synthwave.json +0 -98
- package/src/modes/theme/defaults/light-tokyo-night.json +0 -111
- package/src/modes/theme/defaults/light-wetland.json +0 -91
- package/src/modes/theme/defaults/light-zenith.json +0 -89
- package/src/modes/theme/defaults/limestone.json +0 -94
- package/src/modes/theme/defaults/mahogany.json +0 -97
- package/src/modes/theme/defaults/marble.json +0 -93
- package/src/modes/theme/defaults/obsidian.json +0 -91
- package/src/modes/theme/defaults/onyx.json +0 -91
- package/src/modes/theme/defaults/pearl.json +0 -93
- package/src/modes/theme/defaults/porcelain.json +0 -91
- package/src/modes/theme/defaults/quartz.json +0 -96
- package/src/modes/theme/defaults/sandstone.json +0 -95
- package/src/modes/theme/defaults/titanium.json +0 -90
- package/src/modes/theme/light.json +0 -93
package/src/async/job-manager.ts
CHANGED
|
@@ -78,6 +78,56 @@ export interface AsyncJobFilter {
|
|
|
78
78
|
ownerId?: string;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
function sliceTextFromUtf8ByteOffset(text: string, offsetBytes: number): string {
|
|
82
|
+
if (offsetBytes <= 0) return text;
|
|
83
|
+
let consumedBytes = 0;
|
|
84
|
+
let codeUnitIndex = 0;
|
|
85
|
+
for (const char of text) {
|
|
86
|
+
const charBytes = Buffer.byteLength(char, "utf8");
|
|
87
|
+
if (consumedBytes + charBytes > offsetBytes) break;
|
|
88
|
+
consumedBytes += charBytes;
|
|
89
|
+
codeUnitIndex += char.length;
|
|
90
|
+
}
|
|
91
|
+
return text.slice(codeUnitIndex);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A slice of process-stream output for a background job, as recorded by
|
|
96
|
+
* `appendOutput` / read by `readOutputSince`.
|
|
97
|
+
*
|
|
98
|
+
* The cursor model is monotonic UTF-8 byte offsets. `nextOffset` is the offset
|
|
99
|
+
* to pass to the next read to receive only fresh bytes; `startOffset` is the
|
|
100
|
+
* first byte the manager still retains for this job. When the requested offset
|
|
101
|
+
* is older than `startOffset`, the manager returns the retained tail and sets
|
|
102
|
+
* `truncated: true`.
|
|
103
|
+
*/
|
|
104
|
+
export interface AsyncJobOutputSlice {
|
|
105
|
+
jobId: string;
|
|
106
|
+
status: AsyncJob["status"];
|
|
107
|
+
text: string;
|
|
108
|
+
startOffset: number;
|
|
109
|
+
nextOffset: number;
|
|
110
|
+
truncated: boolean;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Internal: a single chunk of captured stdout/stderr keyed by its byte range. */
|
|
114
|
+
interface AsyncJobOutputChunk {
|
|
115
|
+
startByte: number;
|
|
116
|
+
endByte: number;
|
|
117
|
+
text: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface AsyncJobOutputState {
|
|
121
|
+
chunks: AsyncJobOutputChunk[];
|
|
122
|
+
startOffset: number;
|
|
123
|
+
nextOffset: number;
|
|
124
|
+
retainedBytes: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Default retention cap for per-job captured output. ~512 KiB matches the
|
|
128
|
+
* bash tail-buffer order of magnitude without dominating session memory. */
|
|
129
|
+
export const DEFAULT_JOB_OUTPUT_RETENTION_BYTES = 512 * 1024;
|
|
130
|
+
|
|
81
131
|
export class AsyncJobManager {
|
|
82
132
|
static #instance: AsyncJobManager | undefined;
|
|
83
133
|
|
|
@@ -102,6 +152,9 @@ export class AsyncJobManager {
|
|
|
102
152
|
readonly #suppressedDeliveries = new Set<string>();
|
|
103
153
|
readonly #watchedJobs = new Set<string>();
|
|
104
154
|
readonly #evictionTimers = new Map<string, NodeJS.Timeout>();
|
|
155
|
+
readonly #outputState = new Map<string, AsyncJobOutputState>();
|
|
156
|
+
readonly #ownerCleanups = new Map<string, Set<() => void>>();
|
|
157
|
+
readonly #outputRetentionBytes = DEFAULT_JOB_OUTPUT_RETENTION_BYTES;
|
|
105
158
|
readonly #onJobComplete: AsyncJobManagerOptions["onJobComplete"];
|
|
106
159
|
readonly #maxRunningJobs: number;
|
|
107
160
|
readonly #retentionMs: number;
|
|
@@ -237,6 +290,169 @@ export class AsyncJobManager {
|
|
|
237
290
|
return this.#filterJobs(this.#jobs.values(), filter);
|
|
238
291
|
}
|
|
239
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Append a sanitized process-stream chunk for a background job. Called from
|
|
295
|
+
* the unthrottled bash-executor capture hook (`onRawChunk`) so monitor sees
|
|
296
|
+
* every chunk even when preview/progress callbacks are throttled.
|
|
297
|
+
*
|
|
298
|
+
* Offsets are in UTF-8 bytes. Storing chunk metadata avoids unsafe byte
|
|
299
|
+
* slicing across multibyte characters at read time. The retention window is
|
|
300
|
+
* a per-job rolling cap (`DEFAULT_JOB_OUTPUT_RETENTION_BYTES`); when it
|
|
301
|
+
* overflows, oldest whole chunks are evicted and `startOffset` advances —
|
|
302
|
+
* subsequent reads from a stale offset get `truncated: true`.
|
|
303
|
+
*/
|
|
304
|
+
appendOutput(jobId: string, chunk: string): void {
|
|
305
|
+
if (this.#disposed) return;
|
|
306
|
+
if (!chunk) return;
|
|
307
|
+
if (!this.#jobs.has(jobId)) return;
|
|
308
|
+
|
|
309
|
+
const state = this.#outputState.get(jobId) ?? {
|
|
310
|
+
chunks: [],
|
|
311
|
+
startOffset: 0,
|
|
312
|
+
nextOffset: 0,
|
|
313
|
+
retainedBytes: 0,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
const byteLength = Buffer.byteLength(chunk, "utf8");
|
|
317
|
+
if (byteLength === 0) return;
|
|
318
|
+
|
|
319
|
+
const startByte = state.nextOffset;
|
|
320
|
+
const endByte = startByte + byteLength;
|
|
321
|
+
state.chunks.push({ startByte, endByte, text: chunk });
|
|
322
|
+
state.retainedBytes += byteLength;
|
|
323
|
+
state.nextOffset = endByte;
|
|
324
|
+
|
|
325
|
+
while (state.retainedBytes > this.#outputRetentionBytes && state.chunks.length > 0) {
|
|
326
|
+
const dropped = state.chunks.shift();
|
|
327
|
+
if (!dropped) break;
|
|
328
|
+
const droppedBytes = dropped.endByte - dropped.startByte;
|
|
329
|
+
state.retainedBytes -= droppedBytes;
|
|
330
|
+
state.startOffset = dropped.endByte;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.#outputState.set(jobId, state);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Read fresh process-stream output for a job since `offset` (in UTF-8
|
|
338
|
+
* bytes). Returns `undefined` when the job does not exist or when an
|
|
339
|
+
* `ownerId` filter is set and the job belongs to a different owner — this
|
|
340
|
+
* mirrors the manager-level "not found" pattern used by `cancel`.
|
|
341
|
+
*
|
|
342
|
+
* - `offset < startOffset` returns the retained tail with `truncated: true`.
|
|
343
|
+
* - `offset > nextOffset` clamps to `nextOffset` and returns an empty text
|
|
344
|
+
* slice with `truncated: false`.
|
|
345
|
+
* - Assembled text slices the leading retained chunk at a UTF-8 codepoint
|
|
346
|
+
* boundary when needed, so multibyte characters cannot be split.
|
|
347
|
+
*/
|
|
348
|
+
readOutputSince(jobId: string, offset: number, filter?: AsyncJobFilter): AsyncJobOutputSlice | undefined {
|
|
349
|
+
const job = this.#jobs.get(jobId);
|
|
350
|
+
if (!job) return undefined;
|
|
351
|
+
if (filter?.ownerId && job.ownerId !== filter.ownerId) return undefined;
|
|
352
|
+
|
|
353
|
+
const state = this.#outputState.get(jobId);
|
|
354
|
+
if (!state) {
|
|
355
|
+
return {
|
|
356
|
+
jobId,
|
|
357
|
+
status: job.status,
|
|
358
|
+
text: "",
|
|
359
|
+
startOffset: 0,
|
|
360
|
+
nextOffset: 0,
|
|
361
|
+
truncated: false,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const requestedOffset = Math.max(0, Math.floor(offset));
|
|
366
|
+
if (requestedOffset >= state.nextOffset) {
|
|
367
|
+
return {
|
|
368
|
+
jobId,
|
|
369
|
+
status: job.status,
|
|
370
|
+
text: "",
|
|
371
|
+
startOffset: state.startOffset,
|
|
372
|
+
nextOffset: state.nextOffset,
|
|
373
|
+
truncated: false,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const truncated = requestedOffset < state.startOffset;
|
|
378
|
+
const effectiveOffset = truncated ? state.startOffset : requestedOffset;
|
|
379
|
+
const parts: string[] = [];
|
|
380
|
+
for (const chunk of state.chunks) {
|
|
381
|
+
if (chunk.endByte <= effectiveOffset) continue;
|
|
382
|
+
if (effectiveOffset > chunk.startByte) {
|
|
383
|
+
parts.push(sliceTextFromUtf8ByteOffset(chunk.text, effectiveOffset - chunk.startByte));
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
parts.push(chunk.text);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
jobId,
|
|
391
|
+
status: job.status,
|
|
392
|
+
text: parts.join(""),
|
|
393
|
+
startOffset: state.startOffset,
|
|
394
|
+
nextOffset: state.nextOffset,
|
|
395
|
+
truncated,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Register an owner-scoped cleanup callback. Returns an unregister function.
|
|
401
|
+
*
|
|
402
|
+
* Used by Cron* tools to clear session-scoped timers when the owning agent
|
|
403
|
+
* is torn down. Invoked by `runOwnerCleanups({ ownerId })` before
|
|
404
|
+
* `cancelAll({ ownerId })` so timers cannot register new jobs during
|
|
405
|
+
* teardown.
|
|
406
|
+
*/
|
|
407
|
+
registerOwnerCleanup(ownerId: string, cleanup: () => void): () => void {
|
|
408
|
+
if (!ownerId) {
|
|
409
|
+
throw new Error("registerOwnerCleanup requires a non-empty ownerId");
|
|
410
|
+
}
|
|
411
|
+
let bag = this.#ownerCleanups.get(ownerId);
|
|
412
|
+
if (!bag) {
|
|
413
|
+
bag = new Set();
|
|
414
|
+
this.#ownerCleanups.set(ownerId, bag);
|
|
415
|
+
}
|
|
416
|
+
bag.add(cleanup);
|
|
417
|
+
return () => {
|
|
418
|
+
const current = this.#ownerCleanups.get(ownerId);
|
|
419
|
+
if (!current) return;
|
|
420
|
+
current.delete(cleanup);
|
|
421
|
+
if (current.size === 0) this.#ownerCleanups.delete(ownerId);
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Run and clear every registered cleanup for the given filter. Idempotent
|
|
427
|
+
* and error-isolated: a throwing cleanup does not prevent siblings from
|
|
428
|
+
* running and never escalates to the caller.
|
|
429
|
+
*/
|
|
430
|
+
runOwnerCleanups(filter?: AsyncJobFilter): void {
|
|
431
|
+
const ownerId = filter?.ownerId;
|
|
432
|
+
const targets: Array<[string, Set<() => void>]> = [];
|
|
433
|
+
if (ownerId) {
|
|
434
|
+
const bag = this.#ownerCleanups.get(ownerId);
|
|
435
|
+
if (bag) targets.push([ownerId, bag]);
|
|
436
|
+
} else {
|
|
437
|
+
for (const entry of this.#ownerCleanups.entries()) targets.push(entry);
|
|
438
|
+
}
|
|
439
|
+
for (const [id, bag] of targets) {
|
|
440
|
+
const callbacks = Array.from(bag);
|
|
441
|
+
bag.clear();
|
|
442
|
+
this.#ownerCleanups.delete(id);
|
|
443
|
+
for (const cleanup of callbacks) {
|
|
444
|
+
try {
|
|
445
|
+
cleanup();
|
|
446
|
+
} catch (error) {
|
|
447
|
+
logger.warn("Async job owner cleanup failed", {
|
|
448
|
+
ownerId: id,
|
|
449
|
+
error: error instanceof Error ? error.message : String(error),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
240
456
|
getDeliveryState(filter?: AsyncJobFilter): AsyncJobDeliveryState {
|
|
241
457
|
const deliveries = this.#filterDeliveries(filter);
|
|
242
458
|
const inFlightDeliveries = this.#filterInFlightDeliveries(filter);
|
|
@@ -357,6 +573,10 @@ export class AsyncJobManager {
|
|
|
357
573
|
async dispose(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
358
574
|
this.#disposed = true;
|
|
359
575
|
this.#clearEvictionTimers();
|
|
576
|
+
// Run-and-clear any remaining owner cleanups before tearing down jobs so
|
|
577
|
+
// late-arriving timers cannot register fresh work against a disposed
|
|
578
|
+
// manager. Errors in cleanup callbacks are logged but never escalated.
|
|
579
|
+
this.runOwnerCleanups();
|
|
360
580
|
this.cancelAll();
|
|
361
581
|
await this.waitForAll();
|
|
362
582
|
const drained = await this.drainDeliveries({ timeoutMs: options?.timeoutMs ?? 3_000 });
|
|
@@ -366,6 +586,8 @@ export class AsyncJobManager {
|
|
|
366
586
|
this.#inFlightDeliveries.length = 0;
|
|
367
587
|
this.#suppressedDeliveries.clear();
|
|
368
588
|
this.#watchedJobs.clear();
|
|
589
|
+
this.#outputState.clear();
|
|
590
|
+
this.#ownerCleanups.clear();
|
|
369
591
|
return drained;
|
|
370
592
|
}
|
|
371
593
|
|
|
@@ -399,6 +621,7 @@ export class AsyncJobManager {
|
|
|
399
621
|
this.#jobs.delete(jobId);
|
|
400
622
|
this.#suppressedDeliveries.delete(jobId);
|
|
401
623
|
this.#watchedJobs.delete(jobId);
|
|
624
|
+
this.#outputState.delete(jobId);
|
|
402
625
|
return;
|
|
403
626
|
}
|
|
404
627
|
const existing = this.#evictionTimers.get(jobId);
|
|
@@ -410,6 +633,7 @@ export class AsyncJobManager {
|
|
|
410
633
|
this.#jobs.delete(jobId);
|
|
411
634
|
this.#suppressedDeliveries.delete(jobId);
|
|
412
635
|
this.#watchedJobs.delete(jobId);
|
|
636
|
+
this.#outputState.delete(jobId);
|
|
413
637
|
}, this.#retentionMs);
|
|
414
638
|
timer.unref();
|
|
415
639
|
this.#evictionTimers.set(jobId, timer);
|
package/src/cli/agents-cli.ts
CHANGED
|
@@ -64,6 +64,9 @@ function toFrontmatter(agent: AgentDefinition): Record<string, unknown> {
|
|
|
64
64
|
if (agent.thinkingLevel) frontmatter.thinkingLevel = agent.thinkingLevel;
|
|
65
65
|
if (agent.output !== undefined) frontmatter.output = agent.output;
|
|
66
66
|
if (agent.blocking) frontmatter.blocking = true;
|
|
67
|
+
if (agent.bashAllowedPrefixes && agent.bashAllowedPrefixes.length > 0) {
|
|
68
|
+
frontmatter.bashAllowedPrefixes = agent.bashAllowedPrefixes;
|
|
69
|
+
}
|
|
67
70
|
|
|
68
71
|
return frontmatter;
|
|
69
72
|
}
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { $ } from "bun";
|
|
|
12
12
|
import chalk from "chalk";
|
|
13
13
|
import { theme } from "../modes/theme/theme";
|
|
14
14
|
|
|
15
|
-
const
|
|
15
|
+
const RELEASE_REPO = "Yeachan-Heo/gajae-code";
|
|
16
16
|
const PACKAGE = "@gajae-code/coding-agent";
|
|
17
17
|
|
|
18
18
|
interface ReleaseInfo {
|
|
@@ -122,7 +122,7 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
122
122
|
|
|
123
123
|
if (bunBinDir) return { method: "bun" };
|
|
124
124
|
|
|
125
|
-
throw new Error(`Could not resolve ${APP_NAME} binary path in PATH`);
|
|
125
|
+
throw new Error(formatUnsupportedTargetMessage(`Could not resolve ${APP_NAME} binary path in PATH`));
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/**
|
|
@@ -166,10 +166,7 @@ function compareVersions(a: string, b: string): number {
|
|
|
166
166
|
/**
|
|
167
167
|
* Get the appropriate binary name for this platform.
|
|
168
168
|
*/
|
|
169
|
-
function getBinaryName(): string {
|
|
170
|
-
const platform = process.platform;
|
|
171
|
-
const arch = process.arch;
|
|
172
|
-
|
|
169
|
+
function getBinaryName(platform: NodeJS.Platform = process.platform, arch: string = process.arch): string {
|
|
173
170
|
let os: string;
|
|
174
171
|
switch (platform) {
|
|
175
172
|
case "linux":
|
|
@@ -182,7 +179,7 @@ function getBinaryName(): string {
|
|
|
182
179
|
os = "windows";
|
|
183
180
|
break;
|
|
184
181
|
default:
|
|
185
|
-
throw new Error(`Unsupported platform: ${platform}`);
|
|
182
|
+
throw new Error(formatUnsupportedTargetMessage(`Unsupported platform: ${platform}`));
|
|
186
183
|
}
|
|
187
184
|
|
|
188
185
|
let archName: string;
|
|
@@ -194,7 +191,7 @@ function getBinaryName(): string {
|
|
|
194
191
|
archName = "arm64";
|
|
195
192
|
break;
|
|
196
193
|
default:
|
|
197
|
-
throw new Error(`Unsupported architecture: ${arch}`);
|
|
194
|
+
throw new Error(formatUnsupportedTargetMessage(`Unsupported architecture: ${arch}`));
|
|
198
195
|
}
|
|
199
196
|
|
|
200
197
|
if (os === "windows") {
|
|
@@ -233,6 +230,65 @@ function printVerifiedVersion(expectedVersion: string): void {
|
|
|
233
230
|
console.log(chalk.green(`\n${theme.status.success} Updated to ${expectedVersion}`));
|
|
234
231
|
}
|
|
235
232
|
|
|
233
|
+
function formatBinaryInstallInstruction(platform: NodeJS.Platform = process.platform): string {
|
|
234
|
+
if (platform === "win32") {
|
|
235
|
+
return `For a supported binary install, reinstall with PowerShell: irm https://raw.githubusercontent.com/${RELEASE_REPO}/main/scripts/install.ps1 | iex`;
|
|
236
|
+
}
|
|
237
|
+
return `For a supported binary install, reinstall with: curl -fsSL https://raw.githubusercontent.com/${RELEASE_REPO}/main/scripts/install.sh | sh -s -- --binary`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function formatManualUpdateInstructions(platform: NodeJS.Platform = process.platform): string {
|
|
241
|
+
return [
|
|
242
|
+
`If ${APP_NAME} was installed with Bun, run: bun install -g ${PACKAGE}@latest`,
|
|
243
|
+
`If ${APP_NAME} was installed with npm, pnpm, or another package manager, update it with that same manager.`,
|
|
244
|
+
formatBinaryInstallInstruction(platform),
|
|
245
|
+
].join("\n");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function formatUnsupportedTargetMessage(reason: string, platform: NodeJS.Platform = process.platform): string {
|
|
249
|
+
return `${reason}.\n${formatManualUpdateInstructions(platform)}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function buildReleaseBinaryUrl(
|
|
253
|
+
version: string,
|
|
254
|
+
platform: NodeJS.Platform = process.platform,
|
|
255
|
+
arch: string = process.arch,
|
|
256
|
+
): string {
|
|
257
|
+
const binaryName = getBinaryName(platform, arch);
|
|
258
|
+
const tag = `v${version}`;
|
|
259
|
+
return `https://github.com/${RELEASE_REPO}/releases/download/${tag}/${binaryName}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function formatBinaryDownloadFailureMessage(
|
|
263
|
+
binaryName: string,
|
|
264
|
+
url: string,
|
|
265
|
+
status: string | number,
|
|
266
|
+
platform: NodeJS.Platform = process.platform,
|
|
267
|
+
): string {
|
|
268
|
+
return `Download failed for ${binaryName} from ${url}: ${status}.\n${formatManualUpdateInstructions(platform)}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function formatBinaryDownloadFailureMessageForTest(
|
|
272
|
+
binaryName: string,
|
|
273
|
+
url: string,
|
|
274
|
+
status: string | number,
|
|
275
|
+
platform: NodeJS.Platform = process.platform,
|
|
276
|
+
): string {
|
|
277
|
+
return formatBinaryDownloadFailureMessage(binaryName, url, status, platform);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function buildReleaseBinaryUrlForTest(
|
|
281
|
+
version: string,
|
|
282
|
+
platform: NodeJS.Platform = process.platform,
|
|
283
|
+
arch: string = process.arch,
|
|
284
|
+
): string {
|
|
285
|
+
return buildReleaseBinaryUrl(version, platform, arch);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function formatManualUpdateInstructionsForTest(platform: NodeJS.Platform = process.platform): string {
|
|
289
|
+
return formatManualUpdateInstructions(platform);
|
|
290
|
+
}
|
|
291
|
+
|
|
236
292
|
function formatVerificationFailure(result: InstalledVersionVerification, expectedVersion: string): string {
|
|
237
293
|
if (result.actual) {
|
|
238
294
|
return `${APP_NAME} at ${result.path} still reports ${result.actual} (expected ${expectedVersion})`;
|
|
@@ -250,11 +306,7 @@ async function printVerification(expectedVersion: string): Promise<void> {
|
|
|
250
306
|
return;
|
|
251
307
|
}
|
|
252
308
|
console.log(chalk.yellow(`\nWarning: ${formatVerificationFailure(result, expectedVersion)}`));
|
|
253
|
-
console.log(
|
|
254
|
-
chalk.yellow(
|
|
255
|
-
`You may need to reinstall: curl -fsSL https://raw.githubusercontent.com/can1357/gajae-code/main/scripts/install.sh | sh`,
|
|
256
|
-
),
|
|
257
|
-
);
|
|
309
|
+
console.log(chalk.yellow(formatManualUpdateInstructions()));
|
|
258
310
|
}
|
|
259
311
|
|
|
260
312
|
async function unlinkIfExists(filePath: string): Promise<void> {
|
|
@@ -314,8 +366,7 @@ async function updateViaBun(expectedVersion: string): Promise<void> {
|
|
|
314
366
|
*/
|
|
315
367
|
async function updateViaBinaryAt(targetPath: string, expectedVersion: string): Promise<void> {
|
|
316
368
|
const binaryName = getBinaryName();
|
|
317
|
-
const
|
|
318
|
-
const url = `https://github.com/${REPO}/releases/download/${tag}/${binaryName}`;
|
|
369
|
+
const url = buildReleaseBinaryUrl(expectedVersion);
|
|
319
370
|
|
|
320
371
|
const tempPath = `${targetPath}.new`;
|
|
321
372
|
const backupPath = `${targetPath}.bak`;
|
|
@@ -323,7 +374,7 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
|
|
|
323
374
|
|
|
324
375
|
const response = await fetch(url, { redirect: "follow" });
|
|
325
376
|
if (!response.ok || !response.body) {
|
|
326
|
-
throw new Error(
|
|
377
|
+
throw new Error(formatBinaryDownloadFailureMessage(binaryName, url, response.statusText || response.status));
|
|
327
378
|
}
|
|
328
379
|
const fileStream = fs.createWriteStream(tempPath, { mode: 0o755 });
|
|
329
380
|
await pipeline(response.body, fileStream);
|
|
@@ -337,7 +337,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
337
337
|
|
|
338
338
|
"theme.light": {
|
|
339
339
|
type: "string",
|
|
340
|
-
default: "
|
|
340
|
+
default: "blue-crab",
|
|
341
341
|
ui: {
|
|
342
342
|
tab: "appearance",
|
|
343
343
|
label: "Light Theme",
|
|
@@ -843,6 +843,26 @@ export const SETTINGS_SCHEMA = {
|
|
|
843
843
|
"Maximum wait between retries, in ms. When the provider asks us to wait longer than this and no credential or model fallback succeeds, the request fails fast instead of sleeping (e.g. 3-hour Anthropic rate-limit windows).",
|
|
844
844
|
},
|
|
845
845
|
},
|
|
846
|
+
"retry.requestMaxRetries": {
|
|
847
|
+
type: "number",
|
|
848
|
+
default: 5,
|
|
849
|
+
ui: {
|
|
850
|
+
tab: "model",
|
|
851
|
+
label: "Provider Request Retries",
|
|
852
|
+
description:
|
|
853
|
+
"Maximum provider request retries before a stream is established. Counts retries, not the first attempt. Set to 0 to disable provider request retries.",
|
|
854
|
+
},
|
|
855
|
+
},
|
|
856
|
+
"retry.streamMaxRetries": {
|
|
857
|
+
type: "number",
|
|
858
|
+
default: 5,
|
|
859
|
+
ui: {
|
|
860
|
+
tab: "model",
|
|
861
|
+
label: "Provider Stream Retries",
|
|
862
|
+
description:
|
|
863
|
+
"Maximum provider stream replay retries for replay-safe transient stream failures. Counts retries, not the first attempt. Set to 0 to disable provider stream retries.",
|
|
864
|
+
},
|
|
865
|
+
},
|
|
846
866
|
"retry.fallbackChains": { type: "record", default: {} as Record<string, string[]> },
|
|
847
867
|
"retry.fallbackRevertPolicy": {
|
|
848
868
|
type: "enum",
|
|
@@ -2510,6 +2530,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
2510
2530
|
type: "enum",
|
|
2511
2531
|
values: [
|
|
2512
2532
|
"auto",
|
|
2533
|
+
"duckduckgo",
|
|
2513
2534
|
"exa",
|
|
2514
2535
|
"brave",
|
|
2515
2536
|
"jina",
|
|
@@ -2534,7 +2555,12 @@ export const SETTINGS_SCHEMA = {
|
|
|
2534
2555
|
{
|
|
2535
2556
|
value: "auto",
|
|
2536
2557
|
label: "Auto",
|
|
2537
|
-
description: "
|
|
2558
|
+
description: "Active model's native search if its creds exist, else keyless DuckDuckGo",
|
|
2559
|
+
},
|
|
2560
|
+
{
|
|
2561
|
+
value: "duckduckgo",
|
|
2562
|
+
label: "DuckDuckGo",
|
|
2563
|
+
description: "Keyless default — no API key or OAuth required",
|
|
2538
2564
|
},
|
|
2539
2565
|
{ value: "exa", label: "Exa", description: "Uses Exa API when EXA_API_KEY is set" },
|
|
2540
2566
|
{ value: "brave", label: "Brave", description: "Requires BRAVE_API_KEY" },
|
|
@@ -2833,6 +2859,8 @@ export interface RetrySettings {
|
|
|
2833
2859
|
maxRetries: number;
|
|
2834
2860
|
baseDelayMs: number;
|
|
2835
2861
|
maxDelayMs: number;
|
|
2862
|
+
requestMaxRetries: number;
|
|
2863
|
+
streamMaxRetries: number;
|
|
2836
2864
|
}
|
|
2837
2865
|
|
|
2838
2866
|
export interface MemoriesSettings {
|
package/src/config/settings.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* import { settings } from "./settings";
|
|
6
6
|
*
|
|
7
7
|
* const enabled = settings.get("compaction.enabled"); // sync read
|
|
8
|
-
* settings.set("theme.dark", "
|
|
8
|
+
* settings.set("theme.dark", "red-claw"); // sync write, saves in background
|
|
9
9
|
*
|
|
10
10
|
* For tests:
|
|
11
11
|
* const isolated = Settings.isolated({ "compaction.enabled": false });
|
|
@@ -17,6 +17,7 @@ import * as path from "node:path";
|
|
|
17
17
|
import {
|
|
18
18
|
getAgentDbPath,
|
|
19
19
|
getAgentDir,
|
|
20
|
+
getCustomThemesDir,
|
|
20
21
|
getProjectDir,
|
|
21
22
|
isEnoent,
|
|
22
23
|
logger,
|
|
@@ -100,6 +101,14 @@ function setByPath(obj: RawSettings, segments: string[], value: unknown): void {
|
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
const PATH_SCOPED_ARRAY_SETTINGS = new Set<SettingPath>(["enabledModels", "disabledProviders"]);
|
|
104
|
+
const LEGACY_THEME_NAME_REPLACEMENTS = {
|
|
105
|
+
dark: "red-claw",
|
|
106
|
+
light: "blue-crab",
|
|
107
|
+
} as const;
|
|
108
|
+
|
|
109
|
+
function isLegacyThemeName(name: string): name is keyof typeof LEGACY_THEME_NAME_REPLACEMENTS {
|
|
110
|
+
return name === "dark" || name === "light";
|
|
111
|
+
}
|
|
103
112
|
|
|
104
113
|
type PathScopedStringArrayEntry = {
|
|
105
114
|
path?: unknown;
|
|
@@ -587,6 +596,25 @@ export class Settings {
|
|
|
587
596
|
}
|
|
588
597
|
}
|
|
589
598
|
|
|
599
|
+
#hasCustomThemeFile(name: string): boolean {
|
|
600
|
+
try {
|
|
601
|
+
return fs.existsSync(path.join(getCustomThemesDir(this.#agentDir), `${name}.json`));
|
|
602
|
+
} catch {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
#migrateLegacyBuiltInThemeName(name: string): string {
|
|
608
|
+
if (isLegacyThemeName(name) && !this.#hasCustomThemeFile(name)) {
|
|
609
|
+
return LEGACY_THEME_NAME_REPLACEMENTS[name];
|
|
610
|
+
}
|
|
611
|
+
return name;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
#getThemeSlotForName(name: string): "dark" | "light" {
|
|
615
|
+
return isLightTheme(name, this.#agentDir) ? "light" : "dark";
|
|
616
|
+
}
|
|
617
|
+
|
|
590
618
|
/** Apply schema migrations to raw settings */
|
|
591
619
|
#migrateRawSettings(raw: RawSettings): RawSettings {
|
|
592
620
|
// queueMode -> steeringMode
|
|
@@ -606,13 +634,22 @@ export class Settings {
|
|
|
606
634
|
// Migrate old flat "theme" string to nested theme.dark/theme.light
|
|
607
635
|
if (typeof raw.theme === "string") {
|
|
608
636
|
const oldTheme = raw.theme;
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
637
|
+
const migratedTheme = this.#migrateLegacyBuiltInThemeName(oldTheme);
|
|
638
|
+
if (oldTheme === "dark" && migratedTheme === "red-claw") {
|
|
639
|
+
raw.theme = { dark: migratedTheme };
|
|
640
|
+
} else if (oldTheme === "light" && migratedTheme === "blue-crab") {
|
|
641
|
+
raw.theme = { light: migratedTheme };
|
|
612
642
|
} else {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
643
|
+
const slot = this.#getThemeSlotForName(migratedTheme);
|
|
644
|
+
raw.theme = { [slot]: migratedTheme };
|
|
645
|
+
}
|
|
646
|
+
} else if (raw.theme && typeof raw.theme === "object" && !Array.isArray(raw.theme)) {
|
|
647
|
+
const themeObj = raw.theme as Record<string, unknown>;
|
|
648
|
+
if (typeof themeObj.dark === "string") {
|
|
649
|
+
themeObj.dark = this.#migrateLegacyBuiltInThemeName(themeObj.dark);
|
|
650
|
+
}
|
|
651
|
+
if (typeof themeObj.light === "string") {
|
|
652
|
+
themeObj.light = this.#migrateLegacyBuiltInThemeName(themeObj.light);
|
|
616
653
|
}
|
|
617
654
|
}
|
|
618
655
|
|