@gajae-code/coding-agent 0.9.1 → 0.9.3
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 +36 -0
- package/dist/types/capability/mcp.d.ts +3 -1
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +5 -0
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/commands/mcp.d.ts +1 -1
- package/dist/types/config/model-profile-activation.d.ts +7 -7
- package/dist/types/config/model-registry.d.ts +7 -3
- package/dist/types/config/model-resolver.d.ts +2 -2
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +20 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +9 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -2
- package/dist/types/modes/components/status-line/types.d.ts +6 -0
- package/dist/types/modes/components/status-line.d.ts +1 -0
- package/dist/types/modes/components/thinking-selector.d.ts +6 -1
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +12 -0
- package/dist/types/notifications/config-commands.d.ts +29 -0
- package/dist/types/notifications/index.d.ts +12 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +3 -3
- package/dist/types/notifications/threaded-render.d.ts +2 -0
- package/dist/types/notifications/topic-registry.d.ts +4 -5
- package/dist/types/runtime-credential-selector.d.ts +7 -0
- package/dist/types/runtime-mcp/config-writer.d.ts +4 -3
- package/dist/types/runtime-mcp/types.d.ts +5 -4
- package/dist/types/sdk.d.ts +7 -1
- package/dist/types/session/agent-session.d.ts +22 -3
- package/dist/types/session/session-manager.d.ts +1 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/skill-discovery.d.ts +40 -0
- package/dist/types/tools/skill.d.ts +9 -8
- package/dist/types/utils/linkedom.d.ts +4 -0
- package/dist/types/utils/pasted-image-path.d.ts +5 -3
- package/package.json +11 -7
- package/scripts/build-binary.ts +2 -40
- package/scripts/compile-args.ts +91 -0
- package/scripts/generate-docs-index.ts +1 -0
- package/src/capability/mcp.ts +3 -1
- package/src/cli/args.ts +8 -0
- package/src/cli/mcp-cli.ts +108 -26
- package/src/cli/notify-cli.ts +27 -1
- package/src/cli.ts +26 -0
- package/src/commands/launch.ts +5 -0
- package/src/commands/mcp.ts +8 -8
- package/src/commands/notify.ts +2 -2
- package/src/commands/stats.ts +6 -2
- package/src/config/mcp-schema.json +21 -17
- package/src/config/model-profile-activation.ts +23 -7
- package/src/config/model-registry.ts +21 -4
- package/src/config/model-resolver.ts +5 -16
- package/src/config/settings-schema.ts +3 -2
- package/src/config/settings.ts +24 -1
- package/src/coordinator-mcp/server.ts +7 -4
- package/src/discovery/builtin.ts +14 -0
- package/src/discovery/mcp-json.ts +14 -0
- package/src/extensibility/runtime-skill-discovery.ts +229 -0
- package/src/gjc-runtime/launch-tmux.ts +72 -12
- package/src/gjc-runtime/psmux-detect.ts +17 -2
- package/src/gjc-runtime/state-runtime.ts +105 -12
- package/src/gjc-runtime/team-runtime.ts +35 -3
- package/src/gjc-runtime/workflow-manifest.generated.json +4 -28
- package/src/gjc-runtime/workflow-manifest.ts +1 -7
- package/src/internal-urls/docs-index.generated.ts +3 -2
- package/src/internal-urls/gjc-protocol.ts +14 -1
- package/src/main.ts +26 -3
- package/src/modes/components/assistant-message.ts +1 -1
- package/src/modes/components/settings-selector.ts +3 -3
- package/src/modes/components/status-line/presets.ts +5 -7
- package/src/modes/components/status-line/segments.ts +14 -0
- package/src/modes/components/status-line/types.ts +6 -0
- package/src/modes/components/status-line.ts +7 -1
- package/src/modes/components/thinking-selector.ts +79 -18
- package/src/modes/components/tool-execution.ts +1 -1
- package/src/modes/controllers/event-controller.ts +9 -9
- package/src/modes/controllers/input-controller.ts +52 -11
- package/src/modes/controllers/selector-controller.ts +7 -5
- package/src/modes/interactive-mode.ts +12 -4
- package/src/modes/types.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +202 -47
- package/src/notifications/config-commands.ts +70 -0
- package/src/notifications/index.ts +156 -9
- package/src/notifications/rich-render.ts +1 -0
- package/src/notifications/telegram-daemon-cli.ts +13 -5
- package/src/notifications/telegram-daemon.ts +87 -4
- package/src/notifications/threaded-render.ts +14 -0
- package/src/notifications/topic-registry.ts +9 -8
- package/src/prompts/tools/skill-discovery.md +13 -0
- package/src/prompts/tools/skill.md +2 -2
- package/src/runtime-credential-selector.ts +35 -0
- package/src/runtime-mcp/config-writer.ts +4 -3
- package/src/runtime-mcp/config.ts +1 -0
- package/src/runtime-mcp/types.ts +5 -4
- package/src/sdk.ts +68 -5
- package/src/session/agent-session.ts +159 -21
- package/src/session/session-manager.ts +68 -40
- package/src/system-prompt.ts +4 -6
- package/src/tools/bash-interactive.ts +9 -4
- package/src/tools/browser/readable.ts +2 -2
- package/src/tools/fetch.ts +5 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/skill-discovery.ts +73 -0
- package/src/tools/skill.ts +29 -13
- package/src/utils/linkedom.ts +14 -0
- package/src/utils/pasted-image-path.ts +23 -9
- package/src/web/scrapers/arxiv.ts +2 -2
- package/src/web/scrapers/go-pkg.ts +2 -2
- package/src/web/scrapers/iacr.ts +2 -2
- package/src/web/scrapers/readthedocs.ts +2 -2
- package/src/web/scrapers/twitter.ts +3 -2
- package/src/web/scrapers/wikipedia.ts +2 -2
- package/src/export/html/template.css +0 -1060
- package/src/export/html/template.html +0 -47
- package/src/export/html/template.js +0 -2314
- package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
- package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
- package/vendor/insane-search/engine/tests/test_u1.py +0 -200
- package/vendor/insane-search/engine/tests/test_u4.py +0 -131
- package/vendor/insane-search/engine/tests/test_u5.py +0 -163
- package/vendor/insane-search/engine/tests/test_u7.py +0 -124
- package/vendor/insane-search/engine/tests/test_u8.py +0 -216
|
@@ -45,8 +45,6 @@ import {
|
|
|
45
45
|
MemoryBlobStore,
|
|
46
46
|
parseBlobRef,
|
|
47
47
|
ResidentBlobMissingError,
|
|
48
|
-
resolveImageData,
|
|
49
|
-
resolveImageDataUrl,
|
|
50
48
|
resolveResidentImageDataSync,
|
|
51
49
|
resolveResidentImageDataUrlSync,
|
|
52
50
|
resolveTextBlobSync,
|
|
@@ -973,47 +971,46 @@ export async function loadEntriesFromFile(
|
|
|
973
971
|
}
|
|
974
972
|
|
|
975
973
|
/**
|
|
976
|
-
*
|
|
977
|
-
*
|
|
974
|
+
* Convert legacy persisted blob references in loaded entries into resident sentinels.
|
|
975
|
+
* Images then materialize lazily at provider/display/export chokepoints instead of
|
|
976
|
+
* pinning every historical base64 string for the lifetime of a resumed session.
|
|
978
977
|
*/
|
|
979
978
|
function hasImageUrl(value: unknown): value is { image_url: string | { url?: string } } {
|
|
980
979
|
return typeof value === "object" && value !== null && "image_url" in value;
|
|
981
980
|
}
|
|
982
981
|
|
|
983
|
-
|
|
982
|
+
function residentizePersistedBlobRefs(value: unknown, key?: string): void {
|
|
984
983
|
if (Array.isArray(value)) {
|
|
985
|
-
|
|
984
|
+
for (const item of value) residentizePersistedBlobRefs(item, key);
|
|
986
985
|
return;
|
|
987
986
|
}
|
|
988
987
|
|
|
989
988
|
if (typeof value !== "object" || value === null) return;
|
|
990
989
|
|
|
991
990
|
if (isImageBlock(value) && isBlobRef(value.data)) {
|
|
992
|
-
value.data =
|
|
991
|
+
value.data = residentBlobSentinel("imageData", value.data) as unknown as string;
|
|
993
992
|
}
|
|
994
993
|
|
|
995
994
|
if (hasImageUrl(value)) {
|
|
996
995
|
if (typeof value.image_url === "string" && isBlobRef(value.image_url)) {
|
|
997
|
-
value.image_url =
|
|
996
|
+
value.image_url = residentBlobSentinel("imageUrl", value.image_url) as unknown as string;
|
|
998
997
|
} else if (
|
|
999
998
|
typeof value.image_url === "object" &&
|
|
1000
999
|
value.image_url !== null &&
|
|
1001
1000
|
typeof value.image_url.url === "string" &&
|
|
1002
1001
|
isBlobRef(value.image_url.url)
|
|
1003
1002
|
) {
|
|
1004
|
-
value.image_url.url =
|
|
1003
|
+
value.image_url.url = residentBlobSentinel("imageUrl", value.image_url.url) as unknown as string;
|
|
1005
1004
|
}
|
|
1006
1005
|
}
|
|
1007
1006
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
}),
|
|
1016
|
-
);
|
|
1007
|
+
for (const [childKey, item] of Object.entries(value)) {
|
|
1008
|
+
if (childKey === "data" && typeof item === "string" && isBlobRef(item) && key !== TEXT_CONTENT_KEY) {
|
|
1009
|
+
(value as Record<string, unknown>)[childKey] = residentBlobSentinel("imageUrl", item);
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
residentizePersistedBlobRefs(item, childKey);
|
|
1013
|
+
}
|
|
1017
1014
|
}
|
|
1018
1015
|
|
|
1019
1016
|
/**
|
|
@@ -1035,28 +1032,13 @@ async function runWithConcurrency(tasks: Array<() => Promise<void>>, limit: numb
|
|
|
1035
1032
|
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
1036
1033
|
}
|
|
1037
1034
|
|
|
1038
|
-
async function resolveBlobRefsInEntries(entries: FileEntry[],
|
|
1035
|
+
async function resolveBlobRefsInEntries(entries: FileEntry[], _blobStore: BlobStore): Promise<void> {
|
|
1039
1036
|
const tasks: Array<() => Promise<void>> = [];
|
|
1040
1037
|
|
|
1041
1038
|
for (const entry of entries) {
|
|
1042
1039
|
if (entry.type === "session") continue;
|
|
1043
|
-
|
|
1044
|
-
let contentArray: unknown[] | undefined;
|
|
1045
|
-
if (entry.type === "message" && "content" in entry.message && Array.isArray(entry.message.content)) {
|
|
1046
|
-
contentArray = entry.message.content;
|
|
1047
|
-
} else if (entry.type === "custom_message" && Array.isArray(entry.content)) {
|
|
1048
|
-
contentArray = entry.content;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
1040
|
tasks.push(async () => {
|
|
1052
|
-
|
|
1053
|
-
for (const block of contentArray) {
|
|
1054
|
-
if (isImageBlock(block) && isBlobRef(block.data)) {
|
|
1055
|
-
block.data = await resolveImageData(blobStore, block.data);
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
await resolvePersistedBlobRefs(entry, blobStore);
|
|
1041
|
+
residentizePersistedBlobRefs(entry);
|
|
1060
1042
|
});
|
|
1061
1043
|
}
|
|
1062
1044
|
|
|
@@ -1314,6 +1296,33 @@ function containsResidentSentinel(value: unknown, seen = new WeakSet<object>()):
|
|
|
1314
1296
|
return false;
|
|
1315
1297
|
}
|
|
1316
1298
|
|
|
1299
|
+
function containsResidentImageSentinel(value: unknown, seen = new WeakSet<object>()): boolean {
|
|
1300
|
+
if (value === null || value === undefined || typeof value !== "object") return false;
|
|
1301
|
+
if (isResidentBlobSentinel(value)) return value.kind === "imageUrl" || value.kind === "imageData";
|
|
1302
|
+
if (seen.has(value)) return false;
|
|
1303
|
+
seen.add(value);
|
|
1304
|
+
if (Array.isArray(value)) return value.some(item => containsResidentImageSentinel(item, seen));
|
|
1305
|
+
for (const child of Object.values(value)) {
|
|
1306
|
+
if (containsResidentImageSentinel(child, seen)) return true;
|
|
1307
|
+
}
|
|
1308
|
+
return false;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function collectResidentImageRefs(value: unknown, refs: Set<string>, seen = new WeakSet<object>()): void {
|
|
1312
|
+
if (value === null || value === undefined || typeof value !== "object") return;
|
|
1313
|
+
if (isResidentBlobSentinel(value)) {
|
|
1314
|
+
if (value.kind === "imageUrl" || value.kind === "imageData") refs.add(value.ref);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (seen.has(value)) return;
|
|
1318
|
+
seen.add(value);
|
|
1319
|
+
if (Array.isArray(value)) {
|
|
1320
|
+
for (const item of value) collectResidentImageRefs(item, refs, seen);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
for (const child of Object.values(value)) collectResidentImageRefs(child, refs, seen);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1317
1326
|
/**
|
|
1318
1327
|
* Recursively truncate large strings in an object for session persistence.
|
|
1319
1328
|
* - Truncates any oversized string fields (key-agnostic)
|
|
@@ -4148,6 +4157,22 @@ export class SessionManager {
|
|
|
4148
4157
|
return entry ? materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), new Map()) : undefined;
|
|
4149
4158
|
}
|
|
4150
4159
|
|
|
4160
|
+
getResidentImageBytes(): number {
|
|
4161
|
+
const refs = new Set<string>();
|
|
4162
|
+
for (const entry of this.#fileEntries) collectResidentImageRefs(entry, refs);
|
|
4163
|
+
let bytes = 0;
|
|
4164
|
+
for (const ref of refs) {
|
|
4165
|
+
const hash = parseBlobRef(ref);
|
|
4166
|
+
if (!hash) continue;
|
|
4167
|
+
try {
|
|
4168
|
+
bytes += fs.statSync(path.join(this.#residentImageBlobStore.dir, hash)).size;
|
|
4169
|
+
} catch (err) {
|
|
4170
|
+
if (!isEnoent(err)) throw err;
|
|
4171
|
+
}
|
|
4172
|
+
}
|
|
4173
|
+
return bytes;
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4151
4176
|
/**
|
|
4152
4177
|
* Get the most recent model role from the current session path.
|
|
4153
4178
|
* Returns undefined if no model change has been recorded.
|
|
@@ -4571,11 +4596,14 @@ export class SessionManager {
|
|
|
4571
4596
|
}
|
|
4572
4597
|
this.#materializedEntriesCachePopulateCount++;
|
|
4573
4598
|
const resolvedTextBlobCache = new Map<string, string>();
|
|
4574
|
-
const
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4599
|
+
const sourceEntries = this.#fileEntries.filter((e): e is SessionEntry => e.type !== "session");
|
|
4600
|
+
const materializedEntries = sourceEntries.map(entry =>
|
|
4601
|
+
materializeResidentEntryForReadSync(entry, this.#residentBlobStores(), resolvedTextBlobCache),
|
|
4602
|
+
);
|
|
4603
|
+
if (!sourceEntries.some(entry => containsResidentImageSentinel(entry))) {
|
|
4604
|
+
this.#materializedEntriesCache = materializedEntries;
|
|
4605
|
+
this.#materializedEntriesRevision = this.#entryRevision;
|
|
4606
|
+
}
|
|
4579
4607
|
return materializedEntries;
|
|
4580
4608
|
}
|
|
4581
4609
|
|
package/src/system-prompt.ts
CHANGED
|
@@ -507,7 +507,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
507
507
|
? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).then(result => result.skills)
|
|
508
508
|
: Promise.resolve([]);
|
|
509
509
|
|
|
510
|
-
const [resolvedCustomPrompt, resolvedAppendPrompt, systemPromptCustomization, contextFiles,
|
|
510
|
+
const [resolvedCustomPrompt, resolvedAppendPrompt, systemPromptCustomization, contextFiles, , workspaceTree] =
|
|
511
511
|
await Promise.all([
|
|
512
512
|
withDeadline(
|
|
513
513
|
"customPrompt",
|
|
@@ -581,11 +581,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
581
581
|
description: tools?.get(name)?.description ?? "",
|
|
582
582
|
}));
|
|
583
583
|
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
|
|
587
|
-
const hasRead = tools?.has("read");
|
|
588
|
-
const filteredSkills = hasRead ? skills.filter(skill => skill.hide !== true) : [];
|
|
584
|
+
// Runtime skills are discovered by `skill_discovery` and loaded narrowly through
|
|
585
|
+
// `skill`; do not dump the full custom skill catalog into the core prompt.
|
|
586
|
+
const filteredSkills: Skill[] = [];
|
|
589
587
|
|
|
590
588
|
const effectiveSystemPromptCustomization = dedupePromptSource(systemPromptCustomization, [
|
|
591
589
|
resolvedCustomPrompt,
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
} from "@gajae-code/tui";
|
|
13
13
|
import { sanitizeText } from "@gajae-code/utils";
|
|
14
14
|
import type { Terminal as XtermTerminalType } from "@xterm/headless";
|
|
15
|
-
import xterm from "@xterm/headless";
|
|
16
15
|
import { Settings } from "../config/settings";
|
|
17
16
|
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
18
17
|
import type { Theme } from "../modes/theme/theme";
|
|
@@ -32,8 +31,6 @@ function normalizeCaptureChunk(chunk: string): string {
|
|
|
32
31
|
return sanitizeWithOptionalSixelPassthrough(normalized, sanitizeText);
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
const XtermTerminal = xterm.Terminal;
|
|
36
|
-
|
|
37
34
|
function normalizeInputForPty(data: string, applicationCursorKeysMode: boolean): string {
|
|
38
35
|
const kitty = parseKittySequence(data);
|
|
39
36
|
if (kitty?.eventType === 3) {
|
|
@@ -113,6 +110,7 @@ class BashInteractiveOverlayComponent implements Component {
|
|
|
113
110
|
private readonly command: string,
|
|
114
111
|
private readonly uiTheme: Theme,
|
|
115
112
|
private readonly getTerminalRows: () => number,
|
|
113
|
+
XtermTerminal: typeof XtermTerminalType,
|
|
116
114
|
) {
|
|
117
115
|
this.#terminal = new XtermTerminal({
|
|
118
116
|
cols: 120,
|
|
@@ -305,10 +303,17 @@ export async function runInteractiveBashPty(
|
|
|
305
303
|
headBytes: resolveOutputSinkHeadBytes(settings),
|
|
306
304
|
maxColumns: resolveOutputMaxColumns(settings),
|
|
307
305
|
});
|
|
306
|
+
const { default: xterm } = await import("@xterm/headless");
|
|
307
|
+
const XtermTerminal = xterm.Terminal;
|
|
308
308
|
const result = await ui.custom<BashInteractiveResult>(
|
|
309
309
|
(tui, uiTheme, _keybindings, done) => {
|
|
310
310
|
const session = new PtySession();
|
|
311
|
-
const component = new BashInteractiveOverlayComponent(
|
|
311
|
+
const component = new BashInteractiveOverlayComponent(
|
|
312
|
+
options.command,
|
|
313
|
+
uiTheme,
|
|
314
|
+
() => tui.terminal.rows,
|
|
315
|
+
XtermTerminal,
|
|
316
|
+
);
|
|
312
317
|
component.setSession(session);
|
|
313
318
|
let finished = false;
|
|
314
319
|
const finalize = (run: PtyRunResult) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Readability } from "@mozilla/readability";
|
|
2
|
-
import {
|
|
2
|
+
import { parseHtmlLazy } from "../../utils/linkedom";
|
|
3
3
|
import { htmlToBasicMarkdown } from "../../web/scrapers/types";
|
|
4
4
|
|
|
5
5
|
export type ReadableFormat = "text" | "markdown";
|
|
@@ -31,7 +31,7 @@ export async function extractReadableFromHtml(
|
|
|
31
31
|
url: string,
|
|
32
32
|
format: ReadableFormat,
|
|
33
33
|
): Promise<ReadableResult | null> {
|
|
34
|
-
const { document } =
|
|
34
|
+
const { document } = await parseHtmlLazy(html);
|
|
35
35
|
|
|
36
36
|
// --- Primary: Readability article extraction ---
|
|
37
37
|
const article = new Readability(document).parse();
|
package/src/tools/fetch.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { ImageContent, TextContent } from "@gajae-code/ai";
|
|
|
5
5
|
import { htmlToMarkdown } from "@gajae-code/natives";
|
|
6
6
|
import { type Component, Text } from "@gajae-code/tui";
|
|
7
7
|
import { $which, ptree, truncate } from "@gajae-code/utils";
|
|
8
|
-
import { parseHTML } from "linkedom";
|
|
9
8
|
import type { Settings } from "../config/settings";
|
|
10
9
|
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
11
10
|
import { type Theme, theme } from "../modes/theme/theme";
|
|
@@ -15,6 +14,7 @@ import { DEFAULT_MAX_BYTES, truncateHead } from "../session/streaming-output";
|
|
|
15
14
|
import { renderStatusLine } from "../tui";
|
|
16
15
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
17
16
|
import { formatDimensionNote, resizeImage } from "../utils/image-resize";
|
|
17
|
+
import { parseHtmlLazy } from "../utils/linkedom";
|
|
18
18
|
import { ensureTool } from "../utils/tools-manager";
|
|
19
19
|
import { INSANE_NOTES, tryInsaneFetch } from "../web/insane/bridge";
|
|
20
20
|
import { validatePublicHttpUrl, validatePublicHttpUrlForInsane } from "../web/insane/url-guard";
|
|
@@ -483,9 +483,9 @@ function cleanFeedText(text: string): string {
|
|
|
483
483
|
/**
|
|
484
484
|
* Parse RSS/Atom feed to markdown
|
|
485
485
|
*/
|
|
486
|
-
function parseFeedToMarkdown(content: string, maxItems = 10): string {
|
|
486
|
+
async function parseFeedToMarkdown(content: string, maxItems = 10): Promise<string> {
|
|
487
487
|
try {
|
|
488
|
-
const doc =
|
|
488
|
+
const doc = (await parseHtmlLazy(content)).document;
|
|
489
489
|
|
|
490
490
|
// Try RSS
|
|
491
491
|
const channel = doc.querySelector("channel");
|
|
@@ -1026,7 +1026,7 @@ async function renderUrl(
|
|
|
1026
1026
|
}
|
|
1027
1027
|
|
|
1028
1028
|
if (isFeed || (isXml && (rawContent.includes("<rss") || rawContent.includes("<feed")))) {
|
|
1029
|
-
const parsed = parseFeedToMarkdown(rawContent);
|
|
1029
|
+
const parsed = await parseFeedToMarkdown(rawContent);
|
|
1030
1030
|
const output = finalizeOutput(parsed);
|
|
1031
1031
|
return {
|
|
1032
1032
|
url,
|
|
@@ -1119,7 +1119,7 @@ async function renderUrl(
|
|
|
1119
1119
|
const altResult = await loadPage(resolved, { timeout, signal });
|
|
1120
1120
|
if (altResult.ok && altResult.content.trim().length > 200) {
|
|
1121
1121
|
notes.push(`Used feed alternate: ${resolved}`);
|
|
1122
|
-
const parsed = parseFeedToMarkdown(altResult.content);
|
|
1122
|
+
const parsed = await parseFeedToMarkdown(altResult.content);
|
|
1123
1123
|
const output = finalizeOutput(parsed);
|
|
1124
1124
|
return {
|
|
1125
1125
|
url,
|
package/src/tools/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ import { reportFindingTool } from "./review";
|
|
|
56
56
|
import { SearchTool } from "./search";
|
|
57
57
|
import { SearchToolBm25Tool } from "./search-tool-bm25";
|
|
58
58
|
import { SkillTool } from "./skill";
|
|
59
|
+
import { SkillDiscoveryTool } from "./skill-discovery";
|
|
59
60
|
import { loadSshTool } from "./ssh";
|
|
60
61
|
import { SubagentTool } from "./subagent";
|
|
61
62
|
import { TelegramSendTool } from "./telegram-send";
|
|
@@ -95,6 +96,7 @@ export * from "./review";
|
|
|
95
96
|
export * from "./search";
|
|
96
97
|
export * from "./search-tool-bm25";
|
|
97
98
|
export * from "./skill";
|
|
99
|
+
export * from "./skill-discovery";
|
|
98
100
|
export * from "./ssh";
|
|
99
101
|
export * from "./subagent";
|
|
100
102
|
export * from "./telegram-send";
|
|
@@ -411,6 +413,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
411
413
|
todo_write: s => new TodoWriteTool(s),
|
|
412
414
|
web_search: s => new WebSearchTool(s),
|
|
413
415
|
search_tool_bm25: SearchToolBm25Tool.createIf,
|
|
416
|
+
skill_discovery: SkillDiscoveryTool.createIf,
|
|
414
417
|
telegram_send: TelegramSendTool.createIf,
|
|
415
418
|
write: s => new WriteTool(s),
|
|
416
419
|
skill: SkillTool.createIf,
|
|
@@ -570,6 +573,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
570
573
|
if (name === "search_tool_bm25") return discoveryActive;
|
|
571
574
|
if (name === "calc") return session.settings.get("calc.enabled");
|
|
572
575
|
if (name === "skill") return session.settings.get("skill.enabled");
|
|
576
|
+
if (name === "skill_discovery") return session.settings.get("skill.enabled");
|
|
573
577
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
574
578
|
if (name === "computer") return isComputerCallable(session);
|
|
575
579
|
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@gajae-code/agent-core";
|
|
2
|
+
import { prompt, untilAborted } from "@gajae-code/utils";
|
|
3
|
+
import * as z from "zod/v4";
|
|
4
|
+
import { discoverRuntimeSkills, type RuntimeSkillDiscoveryCandidate } from "../extensibility/runtime-skill-discovery";
|
|
5
|
+
import skillDiscoveryDescription from "../prompts/tools/skill-discovery.md" with { type: "text" };
|
|
6
|
+
import type { ToolSession } from ".";
|
|
7
|
+
|
|
8
|
+
const skillDiscoverySchema = z
|
|
9
|
+
.object({
|
|
10
|
+
query: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe("words to match against skill name, description, source, or use conditions"),
|
|
14
|
+
source: z.enum(["all", "project", "user"]).default("all").optional().describe("skill source scope to search"),
|
|
15
|
+
limit: z.number().min(1).max(50).default(20).optional().describe("maximum results"),
|
|
16
|
+
})
|
|
17
|
+
.strict();
|
|
18
|
+
|
|
19
|
+
export type SkillDiscoveryToolInput = z.infer<typeof skillDiscoverySchema>;
|
|
20
|
+
|
|
21
|
+
export interface SkillDiscoveryToolDetails {
|
|
22
|
+
candidates: RuntimeSkillDiscoveryCandidate[];
|
|
23
|
+
count: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SkillDiscoveryTool implements AgentTool<typeof skillDiscoverySchema, SkillDiscoveryToolDetails> {
|
|
27
|
+
readonly name = "skill_discovery";
|
|
28
|
+
readonly label = "SkillDiscovery";
|
|
29
|
+
readonly summary = "Discover project and user runtime skills by thin metadata";
|
|
30
|
+
readonly loadMode = "essential";
|
|
31
|
+
readonly description: string;
|
|
32
|
+
readonly parameters = skillDiscoverySchema;
|
|
33
|
+
readonly strict = true;
|
|
34
|
+
|
|
35
|
+
readonly #session: ToolSession;
|
|
36
|
+
|
|
37
|
+
constructor(session: ToolSession) {
|
|
38
|
+
this.#session = session;
|
|
39
|
+
this.description = prompt.render(skillDiscoveryDescription);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
#getRuntimeSkillPolicy() {
|
|
43
|
+
return {
|
|
44
|
+
...this.#session.settings.getGroup("skills"),
|
|
45
|
+
disabledExtensions: this.#session.settings.get("disabledExtensions"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static createIf(session: ToolSession): SkillDiscoveryTool | null {
|
|
50
|
+
if (session.settings.get("skill.enabled") === false) return null;
|
|
51
|
+
return new SkillDiscoveryTool(session);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async execute(
|
|
55
|
+
_toolCallId: string,
|
|
56
|
+
input: SkillDiscoveryToolInput,
|
|
57
|
+
signal?: AbortSignal,
|
|
58
|
+
): Promise<AgentToolResult<SkillDiscoveryToolDetails>> {
|
|
59
|
+
return untilAborted(signal, async () => {
|
|
60
|
+
const candidates = await discoverRuntimeSkills({
|
|
61
|
+
cwd: this.#session.cwd,
|
|
62
|
+
query: input.query,
|
|
63
|
+
source: input.source ?? "all",
|
|
64
|
+
limit: input.limit,
|
|
65
|
+
policy: this.#getRuntimeSkillPolicy(),
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: JSON.stringify({ candidates, count: candidates.length }, null, 2) }],
|
|
69
|
+
details: { candidates, count: candidates.length },
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/tools/skill.ts
CHANGED
|
@@ -4,25 +4,28 @@
|
|
|
4
4
|
* Lets the agent hand off to another available skill in the current turn. The
|
|
5
5
|
* callee's SKILL.md is dispatched through the same custom-message path used by
|
|
6
6
|
* `/skill:<name>` typing, as a user-attribution message delivered same-turn
|
|
7
|
-
* (without `deliverAs: "nextTurn"`).
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* (without `deliverAs: "nextTurn"`). When the caller is one of the canonical
|
|
8
|
+
* workflow skills, the tool first runs the native state handoff so workflow
|
|
9
|
+
* mode-state and `skill-active-state.json` transition atomically. Runtime
|
|
10
|
+
* project/user skills have no native workflow mode-state, so they are dispatched
|
|
11
|
+
* directly and tracked by the prompt observer instead.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
* `{complete, completed, handoff, failed, cancelled, canceled, inactive}`.
|
|
14
|
-
* agent declares readiness either by writing `current_phase: "handoff"` to
|
|
15
|
-
* mode-state or by running the handoff verb directly.
|
|
13
|
+
* Canonical workflow chaining is refused unless the caller's `current_phase` is
|
|
14
|
+
* in `{complete, completed, handoff, failed, cancelled, canceled, inactive}`.
|
|
15
|
+
* The agent declares readiness either by writing `current_phase: "handoff"` to
|
|
16
|
+
* its mode-state or by running the handoff verb directly.
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import type { AgentTool, AgentToolResult } from "@gajae-code/agent-core";
|
|
19
20
|
import { prompt, untilAborted } from "@gajae-code/utils";
|
|
20
21
|
import * as z from "zod/v4";
|
|
21
22
|
import { resolveSubskillActivationForSkillInvocation } from "../extensibility/gjc-plugins";
|
|
23
|
+
import { findRuntimeSkillByName } from "../extensibility/runtime-skill-discovery";
|
|
22
24
|
import { buildSkillPromptMessage } from "../extensibility/skills";
|
|
23
25
|
import { runNativeStateCommand } from "../gjc-runtime/state-runtime";
|
|
24
26
|
import skillDescription from "../prompts/tools/skill.md" with { type: "text" };
|
|
25
27
|
import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
|
|
28
|
+
import { isCanonicalGjcWorkflowSkill } from "../skill-state/active-state";
|
|
26
29
|
import type { ToolSession } from ".";
|
|
27
30
|
import { ToolError } from "./tool-errors";
|
|
28
31
|
|
|
@@ -64,6 +67,13 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
|
|
|
64
67
|
this.description = prompt.render(skillDescription);
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
#getRuntimeSkillPolicy() {
|
|
71
|
+
return {
|
|
72
|
+
...this.#session.settings.getGroup("skills"),
|
|
73
|
+
disabledExtensions: this.#session.settings.get("disabledExtensions"),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
static createIf(session: ToolSession): SkillTool | null {
|
|
68
78
|
// The tool can only chain when the session can deliver the same-turn
|
|
69
79
|
// custom message. Without `sendCustomMessage` (e.g. minimal tool
|
|
@@ -110,16 +120,22 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
|
|
|
110
120
|
);
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
const skill =
|
|
123
|
+
const skill =
|
|
124
|
+
skills.find(s => s.name === requestedName) ??
|
|
125
|
+
(await findRuntimeSkillByName(this.#session.cwd, requestedName, this.#getRuntimeSkillPolicy()));
|
|
114
126
|
if (!skill) {
|
|
115
127
|
const available = skills.map(s => s.name).sort();
|
|
116
|
-
const hint =
|
|
128
|
+
const hint =
|
|
129
|
+
available.length > 0
|
|
130
|
+
? ` Available: ${available.join(", ")}. Use skill_discovery to find project/user runtime skills.`
|
|
131
|
+
: " Use skill_discovery to find project/user runtime skills.";
|
|
117
132
|
throw new ToolError(`skill tool: unknown skill "${requestedName}".${hint}`);
|
|
118
133
|
}
|
|
119
134
|
|
|
120
|
-
// Phase guard + atomic handoff
|
|
121
|
-
//
|
|
122
|
-
|
|
135
|
+
// Phase guard + atomic native handoff only apply to canonical workflow
|
|
136
|
+
// skills. Runtime project/user skills do not have a native mode-state,
|
|
137
|
+
// so there is no `gjc state <skill>` command to run for them.
|
|
138
|
+
if (activeSkill && isCanonicalGjcWorkflowSkill(activeSkill)) {
|
|
123
139
|
const phase = (this.#session.getActiveSkillPhase?.() ?? "running").trim().toLowerCase();
|
|
124
140
|
if (!TERMINAL_PHASES.has(phase)) {
|
|
125
141
|
throw new ToolError(
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type LinkedomModule = typeof import("linkedom");
|
|
2
|
+
|
|
3
|
+
type ParseHtml = LinkedomModule["parseHTML"];
|
|
4
|
+
|
|
5
|
+
let parseHtml: ParseHtml | undefined;
|
|
6
|
+
let linkedomImport: Promise<LinkedomModule> | undefined;
|
|
7
|
+
|
|
8
|
+
export async function parseHtmlLazy(markup: string): Promise<ReturnType<ParseHtml>> {
|
|
9
|
+
if (!parseHtml) {
|
|
10
|
+
linkedomImport ??= import("linkedom");
|
|
11
|
+
parseHtml = (await linkedomImport).parseHTML;
|
|
12
|
+
}
|
|
13
|
+
return parseHtml(markup);
|
|
14
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolve pasted editor text to an image file path.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Some terminal/clipboard integrations paste a temporary filesystem path after a
|
|
5
|
+
* copied image is pasted into the editor (for example `/tmp/clipboard-...png`).
|
|
6
|
+
* Only those recognized clipboard temp files are auto-attached and replaced with
|
|
7
|
+
* an `[image N] source="/path"` reference so the model receives both the image
|
|
8
|
+
* payload and the raw temp file path. Ordinary image paths remain literal prompt
|
|
9
|
+
* text; users attach saved files explicitly with `@path/to/image.png`.
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from "node:fs";
|
|
12
12
|
import * as os from "node:os";
|
|
@@ -14,6 +14,7 @@ import * as path from "node:path";
|
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
15
|
|
|
16
16
|
const IMAGE_FILE_EXTENSION_PATTERN = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
17
|
+
const CLIPBOARD_TEMP_BASENAME_PATTERN = /^clipboard-\d{4}-\d{2}-\d{2}-\d{6}-[A-Za-z0-9-]+\.(?:png|jpe?g|gif|webp)$/i;
|
|
17
18
|
|
|
18
19
|
export interface DecodePastedPathOptions {
|
|
19
20
|
/**
|
|
@@ -150,16 +151,25 @@ function hasSupportedImageMagic(filePath: string): boolean {
|
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
153
|
|
|
154
|
+
function isRecognizedClipboardTempPath(filePath: string): boolean {
|
|
155
|
+
const resolved = path.resolve(filePath);
|
|
156
|
+
const tempRoot = path.resolve(os.tmpdir());
|
|
157
|
+
if (resolved !== tempRoot && !resolved.startsWith(`${tempRoot}${path.sep}`)) return false;
|
|
158
|
+
return CLIPBOARD_TEMP_BASENAME_PATTERN.test(path.basename(resolved));
|
|
159
|
+
}
|
|
160
|
+
|
|
153
161
|
/**
|
|
154
|
-
* Returns the resolved path when the whole pasted text is a
|
|
155
|
-
* existing image file
|
|
156
|
-
*
|
|
162
|
+
* Returns the resolved path when the whole pasted text is a recognized clipboard
|
|
163
|
+
* temp path to an existing image file, otherwise `undefined` (the paste is
|
|
164
|
+
* inserted as text). Arbitrary saved image paths are not auto-attached here;
|
|
165
|
+
* users attach them explicitly with `@path/to/image.png`.
|
|
157
166
|
*/
|
|
158
167
|
export function resolvePastedImagePath(text: string, options?: ResolvePastedImagePathOptions): string | undefined {
|
|
159
168
|
const candidate = decodePastedPathCandidate(text, options);
|
|
160
169
|
if (!candidate || !IMAGE_FILE_EXTENSION_PATTERN.test(candidate)) return undefined;
|
|
161
170
|
|
|
162
171
|
const resolved = path.resolve(options?.cwd ?? process.cwd(), candidate);
|
|
172
|
+
if (!isRecognizedClipboardTempPath(resolved)) return undefined;
|
|
163
173
|
try {
|
|
164
174
|
if (!fs.statSync(resolved).isFile()) return undefined;
|
|
165
175
|
} catch {
|
|
@@ -168,3 +178,7 @@ export function resolvePastedImagePath(text: string, options?: ResolvePastedImag
|
|
|
168
178
|
if (!hasSupportedImageMagic(resolved)) return undefined;
|
|
169
179
|
return resolved;
|
|
170
180
|
}
|
|
181
|
+
|
|
182
|
+
export function formatPastedImageReference(placeholder: string, imagePath: string): string {
|
|
183
|
+
return `${placeholder} source=${JSON.stringify(imagePath)}`;
|
|
184
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseHtmlLazy } from "../../utils/linkedom";
|
|
2
2
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
3
|
import { buildResult, loadPage } from "./types";
|
|
4
4
|
import { convertWithMarkit, fetchBinary } from "./utils";
|
|
@@ -31,7 +31,7 @@ export const handleArxiv: SpecialHandler = async (
|
|
|
31
31
|
if (!result.ok) return null;
|
|
32
32
|
|
|
33
33
|
// Parse the Atom feed response
|
|
34
|
-
const doc =
|
|
34
|
+
const doc = (await parseHtmlLazy(result.content)).document;
|
|
35
35
|
const entry = doc.querySelector("entry");
|
|
36
36
|
|
|
37
37
|
if (!entry) return null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { tryParseJson } from "@gajae-code/utils";
|
|
2
|
-
import {
|
|
2
|
+
import { parseHtmlLazy } from "../../utils/linkedom";
|
|
3
3
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
4
4
|
import { buildResult, htmlToBasicMarkdown, loadPage } from "./types";
|
|
5
5
|
|
|
@@ -97,7 +97,7 @@ export const handleGoPkg: SpecialHandler = async (
|
|
|
97
97
|
});
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
const doc =
|
|
100
|
+
const doc = (await parseHtmlLazy(pageResult.content)).document;
|
|
101
101
|
|
|
102
102
|
// Extract actual module path from breadcrumb or header
|
|
103
103
|
const breadcrumb = doc.querySelector(".go-Breadcrumb");
|
package/src/web/scrapers/iacr.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { parseHtmlLazy } from "../../utils/linkedom";
|
|
2
2
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
3
|
import { buildResult, loadPage } from "./types";
|
|
4
4
|
import { convertWithMarkit, fetchBinary } from "./utils";
|
|
@@ -30,7 +30,7 @@ export const handleIacr: SpecialHandler = async (
|
|
|
30
30
|
|
|
31
31
|
if (!result.ok) return null;
|
|
32
32
|
|
|
33
|
-
const doc =
|
|
33
|
+
const doc = (await parseHtmlLazy(result.content)).document;
|
|
34
34
|
|
|
35
35
|
// Extract metadata from the page
|
|
36
36
|
const title =
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Read the Docs handler for web-fetch
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
4
|
+
import { parseHtmlLazy } from "../../utils/linkedom";
|
|
5
5
|
import { buildResult, htmlToBasicMarkdown, loadPage, type RenderResult, type SpecialHandler } from "./types";
|
|
6
6
|
|
|
7
7
|
export const handleReadTheDocs: SpecialHandler = async (
|
|
@@ -39,7 +39,7 @@ export const handleReadTheDocs: SpecialHandler = async (
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Parse HTML
|
|
42
|
-
const root =
|
|
42
|
+
const root = (await parseHtmlLazy(result.content)).document;
|
|
43
43
|
|
|
44
44
|
// Extract main content from common Read the Docs selectors
|
|
45
45
|
let mainContent =
|