@gajae-code/coding-agent 0.11.6 → 0.11.8
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 +92 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/cli/args.d.ts +0 -2
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +34 -4
- package/dist/types/config/model-profile-activation.d.ts +4 -3
- package/dist/types/config/model-profile-contract.d.ts +38 -0
- package/dist/types/config/settings-schema.d.ts +70 -7
- package/dist/types/edit/modes/patch.d.ts +13 -0
- package/dist/types/edit/path-mutation-lock.d.ts +15 -0
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -2
- package/dist/types/gjc-runtime/repository-binding.d.ts +63 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +6 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +43 -5
- package/dist/types/main.d.ts +32 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +2 -1
- package/dist/types/modes/components/tool-status-header.d.ts +4 -2
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +7 -2
- package/dist/types/modes/prompt-action-autocomplete.d.ts +6 -1
- package/dist/types/modes/prompt-suggestion-controller.d.ts +35 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +2 -2
- package/dist/types/modes/types.d.ts +4 -0
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +2 -1
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/runtime-mcp/manager.d.ts +1 -1
- package/dist/types/runtime-mcp/oauth-flow.d.ts +0 -5
- package/dist/types/runtime-mcp/plugin-network-boundary.d.ts +14 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -1
- package/dist/types/sdk/bus/config-commands.d.ts +2 -2
- package/dist/types/sdk/bus/config.d.ts +2 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +26 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-orchestration.d.ts +1 -0
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +105 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +18 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +189 -4
- package/dist/types/sdk/bus/telegram-reference.d.ts +2 -0
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +1 -1
- package/dist/types/sdk/host/query/handlers.d.ts +9 -0
- package/dist/types/sdk/index.d.ts +2 -0
- package/dist/types/sdk/prompt-status.d.ts +88 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- package/dist/types/sdk/startup-capability.d.ts +3 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +30 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +32 -9
- package/dist/types/session/internal/managed-session-storage.d.ts +27 -10
- package/dist/types/session/internal/native-publish-outcome.d.ts +48 -0
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/session/session-storage.d.ts +18 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/task/types.d.ts +94 -0
- package/dist/types/tools/ask.d.ts +454 -174
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +63 -1
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/prompt-suggestion.d.ts +33 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/capture-platform-shortcut-labels-showcase.ts +342 -0
- package/scripts/dogfood-repository-binding.ts +209 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +4 -4
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/args.ts +2 -92
- package/src/cli/notify-cli.ts +5 -38
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +52 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +44 -11
- package/src/config/keybindings.ts +158 -25
- package/src/config/model-profile-activation.ts +7 -18
- package/src/config/model-profile-contract.ts +161 -0
- package/src/config/model-profiles.ts +18 -0
- package/src/config/model-registry.ts +11 -1
- package/src/config/settings-schema.ts +73 -7
- package/src/coordinator-mcp/model-preset.ts +21 -74
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +2 -2
- package/src/defaults/gjc/skills/ralplan/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/edit/index.ts +3 -6
- package/src/edit/modes/patch.ts +38 -1
- package/src/edit/modes/replace.ts +42 -0
- package/src/edit/path-mutation-lock.ts +112 -0
- package/src/eval/py/executor.ts +30 -11
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/prelude.py +2 -2
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/eval/py/tool-bridge.ts +34 -18
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +2 -1
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/launch-tmux.ts +1 -1
- package/src/gjc-runtime/managed-owner-supervisor.ts +83 -4
- package/src/gjc-runtime/ralplan-runtime.ts +75 -3
- package/src/gjc-runtime/repository-binding.ts +267 -0
- package/src/gjc-runtime/session-state-sidecar.ts +25 -9
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -1
- package/src/gjc-runtime/tmux-sessions.ts +45 -6
- package/src/gjc-runtime/ultragoal-guard.ts +93 -15
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +456 -31
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +12 -10
- package/src/internal-urls/local-protocol.ts +64 -21
- package/src/internal-urls/mcp-protocol.ts +73 -11
- package/src/main.ts +111 -37
- package/src/modes/DESIGN.md +5 -0
- package/src/modes/acp/acp-agent.ts +50 -16
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +73 -11
- package/src/modes/components/plan-preview-overlay.ts +1 -1
- package/src/modes/components/runtime-mcp-add-wizard.ts +5 -0
- package/src/modes/components/tool-status-header.ts +25 -4
- package/src/modes/components/welcome.ts +22 -18
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +97 -54
- package/src/modes/controllers/event-controller.ts +10 -0
- package/src/modes/controllers/input-controller.ts +38 -2
- package/src/modes/controllers/runtime-mcp-command-controller.ts +29 -6
- package/src/modes/controllers/selector-controller.ts +149 -63
- package/src/modes/interactive-mode.ts +57 -34
- package/src/modes/prompt-action-autocomplete.ts +32 -13
- package/src/modes/prompt-suggestion-controller.ts +96 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +2 -2
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/hotkeys-markdown.ts +64 -36
- package/src/modes/utils/ui-helpers.ts +5 -3
- package/src/prompts/agents/architect.md +0 -1
- package/src/prompts/agents/critic.md +0 -1
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/system/prompt-suggestion-system.md +32 -0
- package/src/prompts/system/system-prompt.md +6 -4
- package/src/prompts/tools/cron.md +3 -1
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/manager.ts +51 -53
- package/src/runtime-mcp/oauth-flow.ts +0 -72
- package/src/runtime-mcp/plugin-network-boundary.ts +101 -0
- package/src/runtime-mcp/transports/http.ts +63 -22
- package/src/sdk/broker/broker.ts +34 -20
- package/src/sdk/broker/lifecycle.ts +185 -36
- package/src/sdk/bus/chat-daemon-control.ts +19 -5
- package/src/sdk/bus/config-commands.ts +2 -2
- package/src/sdk/bus/config.ts +9 -1
- package/src/sdk/bus/conversation-store.ts +40 -6
- package/src/sdk/bus/index.ts +244 -70
- package/src/sdk/bus/lifecycle-commands.ts +63 -10
- package/src/sdk/bus/lifecycle-control-runtime.ts +11 -2
- package/src/sdk/bus/lifecycle-orchestrator.ts +32 -1
- package/src/sdk/bus/notification-orchestration.ts +12 -0
- package/src/sdk/bus/notification-service.ts +68 -20
- package/src/sdk/bus/prompt-reconciliation.ts +239 -0
- package/src/sdk/bus/recent-activity.ts +90 -26
- package/src/sdk/bus/telegram-daemon-contract.ts +18 -3
- package/src/sdk/bus/telegram-daemon-control.ts +147 -35
- package/src/sdk/bus/telegram-daemon.ts +2463 -450
- package/src/sdk/bus/telegram-reference.ts +20 -2
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/cli/session-cli.ts +15 -2
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +1 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +2 -6
- package/src/sdk/host/query/handlers.ts +70 -5
- package/src/sdk/index.ts +12 -0
- package/src/sdk/prompt-status.ts +97 -0
- package/src/sdk/protocol/operation-inventory.generated.json +58 -0
- package/src/sdk/protocol/operation-registry.ts +18 -4
- package/src/sdk/session-directory.ts +5 -1
- package/src/sdk/session.ts +31 -5
- package/src/sdk/startup-capability.ts +9 -1
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +292 -154
- package/src/session/blob-store.ts +25 -7
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +1163 -137
- package/src/session/internal/managed-session-storage.ts +397 -234
- package/src/session/internal/native-publish-outcome.ts +266 -0
- package/src/session/session-manager.ts +620 -403
- package/src/session/session-storage.ts +128 -11
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/task/gjc-command.ts +2 -2
- package/src/task/index.ts +121 -15
- package/src/task/receipt.ts +3 -0
- package/src/task/types.ts +34 -3
- package/src/tools/ask.ts +199 -89
- package/src/tools/ast-edit.ts +2 -2
- package/src/tools/bash-pty-selection.ts +2 -2
- package/src/tools/browser/screenshot-format.ts +6 -1
- package/src/tools/cron.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +476 -23
- package/src/tools/index.ts +4 -0
- package/src/tools/read.ts +2 -0
- package/src/tools/render-utils.ts +7 -1
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/edit-mode.ts +2 -2
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/prompt-suggestion.ts +265 -0
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/utils/sixel.ts +3 -3
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +140 -27
- package/src/web/scrapers/docs-rs.ts +30 -5
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -21,6 +21,13 @@ export type SkillDiscoveryToolInput = z.infer<typeof skillDiscoverySchema>;
|
|
|
21
21
|
export interface SkillDiscoveryToolDetails {
|
|
22
22
|
candidates: RuntimeSkillDiscoveryCandidate[];
|
|
23
23
|
count: number;
|
|
24
|
+
/**
|
|
25
|
+
* Present only when zero candidates were returned AND discovery config gates
|
|
26
|
+
* (`skills.enabled` / `skills.enablePiProject` / `skills.enablePiUser`)
|
|
27
|
+
* prevented some or all of the requested scope from being searched. Without
|
|
28
|
+
* this, a disabled config is indistinguishable from "no skills exist".
|
|
29
|
+
*/
|
|
30
|
+
notice?: string;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
export class SkillDiscoveryTool implements AgentTool<typeof skillDiscoverySchema, SkillDiscoveryToolDetails> {
|
|
@@ -57,17 +64,46 @@ export class SkillDiscoveryTool implements AgentTool<typeof skillDiscoverySchema
|
|
|
57
64
|
signal?: AbortSignal,
|
|
58
65
|
): Promise<AgentToolResult<SkillDiscoveryToolDetails>> {
|
|
59
66
|
return untilAborted(signal, async () => {
|
|
67
|
+
const source = input.source ?? "all";
|
|
60
68
|
const candidates = await discoverRuntimeSkills({
|
|
61
69
|
cwd: this.#session.cwd,
|
|
62
70
|
query: input.query,
|
|
63
|
-
source
|
|
71
|
+
source,
|
|
64
72
|
limit: input.limit,
|
|
65
73
|
policy: this.#getRuntimeSkillPolicy(),
|
|
66
74
|
});
|
|
75
|
+
const details: SkillDiscoveryToolDetails = { candidates, count: candidates.length };
|
|
76
|
+
if (candidates.length === 0) {
|
|
77
|
+
const notice = this.#disabledPolicyNotice(source);
|
|
78
|
+
if (notice) details.notice = notice;
|
|
79
|
+
}
|
|
67
80
|
return {
|
|
68
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
69
|
-
details
|
|
81
|
+
content: [{ type: "text", text: JSON.stringify(details, null, 2) }],
|
|
82
|
+
details,
|
|
70
83
|
};
|
|
71
84
|
});
|
|
72
85
|
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Explain an empty result that is caused by disabled discovery config rather
|
|
89
|
+
* than by an actually empty skill catalog.
|
|
90
|
+
*/
|
|
91
|
+
#disabledPolicyNotice(source: "all" | "project" | "user"): string | undefined {
|
|
92
|
+
const policy = this.#getRuntimeSkillPolicy();
|
|
93
|
+
if (policy.enabled !== true) {
|
|
94
|
+
return "Runtime skill discovery is disabled: `skills.enabled` is false, so no skill directories were searched. Enable it with `gjc config set skills.enabled true` (project and user scopes additionally require `skills.enablePiProject` / `skills.enablePiUser`).";
|
|
95
|
+
}
|
|
96
|
+
const skipped: string[] = [];
|
|
97
|
+
const commands: string[] = [];
|
|
98
|
+
if ((source === "all" || source === "project") && policy.enablePiProject !== true) {
|
|
99
|
+
skipped.push("project (`skills.enablePiProject` is false)");
|
|
100
|
+
commands.push("`gjc config set skills.enablePiProject true`");
|
|
101
|
+
}
|
|
102
|
+
if ((source === "all" || source === "user") && policy.enablePiUser !== true) {
|
|
103
|
+
skipped.push("user (`skills.enablePiUser` is false)");
|
|
104
|
+
commands.push("`gjc config set skills.enablePiUser true`");
|
|
105
|
+
}
|
|
106
|
+
if (skipped.length === 0) return undefined;
|
|
107
|
+
return `Skill discovery skipped disabled scope(s): ${skipped.join(", ")}. Enable them with ${commands.join(" and ")}.`;
|
|
108
|
+
}
|
|
73
109
|
}
|
|
@@ -272,6 +272,7 @@ const COMMENT_OR_TERMINATOR_ERROR =
|
|
|
272
272
|
"SQLite 'where' clause must not contain comments or statement terminators; use '?q=SELECT ...' for raw SQL";
|
|
273
273
|
const FORBIDDEN_KEYWORD_ERROR =
|
|
274
274
|
"SQLite 'where' clause must not contain LIMIT/OFFSET/UNION/INTERSECT/EXCEPT/ATTACH/DETACH/PRAGMA; use '?q=SELECT ...' for raw SQL";
|
|
275
|
+
const RAW_QUERY_ERROR = "SQLite raw queries must contain exactly one explicit SELECT statement";
|
|
275
276
|
|
|
276
277
|
/**
|
|
277
278
|
* Scans a `where=` clause character-by-character, tracking single- and double-quoted
|
|
@@ -363,6 +364,66 @@ function validateWhereClause(where: string | undefined): string | undefined {
|
|
|
363
364
|
return trimmed;
|
|
364
365
|
}
|
|
365
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Keep raw reads deliberately narrower than SQLite's full grammar. Comments are
|
|
369
|
+
* rejected, and a statement terminator is accepted only as the final
|
|
370
|
+
* non-whitespace character. Quoted semicolons remain ordinary query data.
|
|
371
|
+
*/
|
|
372
|
+
export function validateSqliteReadQuery(sql: string): string {
|
|
373
|
+
const trimmed = sql.trim();
|
|
374
|
+
if (!/^SELECT\b/i.test(trimmed)) {
|
|
375
|
+
throw new ToolError(RAW_QUERY_ERROR);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
let quoteEnd: "'" | '"' | "`" | "]" | null = null;
|
|
379
|
+
for (let index = 0; index < trimmed.length; index++) {
|
|
380
|
+
const char = trimmed[index];
|
|
381
|
+
const next = trimmed[index + 1];
|
|
382
|
+
if (char === "\0") {
|
|
383
|
+
throw new ToolError(RAW_QUERY_ERROR);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (quoteEnd) {
|
|
387
|
+
if (char === quoteEnd) {
|
|
388
|
+
if (quoteEnd !== "]" && next === quoteEnd) {
|
|
389
|
+
index += 1;
|
|
390
|
+
} else {
|
|
391
|
+
quoteEnd = null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
398
|
+
quoteEnd = char;
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (char === "[") {
|
|
402
|
+
quoteEnd = "]";
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if ((char === "-" && next === "-") || (char === "/" && next === "*") || (char === "*" && next === "/")) {
|
|
406
|
+
throw new ToolError(RAW_QUERY_ERROR);
|
|
407
|
+
}
|
|
408
|
+
if (char === ";" && trimmed.slice(index + 1).trim().length > 0) {
|
|
409
|
+
throw new ToolError(RAW_QUERY_ERROR);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (quoteEnd) {
|
|
414
|
+
throw new ToolError(RAW_QUERY_ERROR);
|
|
415
|
+
}
|
|
416
|
+
return trimmed;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function enforceSqliteQueryOnly(db: Database): void {
|
|
420
|
+
db.run("PRAGMA query_only = ON");
|
|
421
|
+
const enabled = db.prepare<{ query_only: number }, []>("PRAGMA query_only").get()?.query_only;
|
|
422
|
+
if (enabled !== 1) {
|
|
423
|
+
throw new ToolError("SQLite read connection could not enable query-only mode");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
366
427
|
function normalizeWriteValue(value: unknown, column: string): SqliteBinding {
|
|
367
428
|
if (value === null) return null;
|
|
368
429
|
if (
|
|
@@ -444,10 +505,7 @@ export function parseSqliteSelector(subPath: string, queryString: string): Sqlit
|
|
|
444
505
|
if (normalizedSubPath || otherKeys.length > 0) {
|
|
445
506
|
throw new ToolError("SQLite raw queries cannot be combined with table selectors or pagination");
|
|
446
507
|
}
|
|
447
|
-
|
|
448
|
-
throw new ToolError("SQLite query parameter 'q' cannot be empty");
|
|
449
|
-
}
|
|
450
|
-
return { kind: "raw", sql: rawQuery };
|
|
508
|
+
return { kind: "raw", sql: validateSqliteReadQuery(rawQuery) };
|
|
451
509
|
}
|
|
452
510
|
|
|
453
511
|
if (!normalizedSubPath) {
|
|
@@ -602,7 +660,7 @@ export function executeReadQuery(
|
|
|
602
660
|
sql: string,
|
|
603
661
|
maxRows: number = MAX_RAW_QUERY_ROWS,
|
|
604
662
|
): { columns: string[]; rows: Record<string, unknown>[]; truncated: boolean } {
|
|
605
|
-
const statement = db.prepare<SqliteRow, []>(sql);
|
|
663
|
+
const statement = db.prepare<SqliteRow, []>(validateSqliteReadQuery(sql));
|
|
606
664
|
if (statement.paramsCount > 0) {
|
|
607
665
|
throw new ToolError("SQLite raw queries do not support bound parameters");
|
|
608
666
|
}
|
package/src/utils/edit-mode.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $pickenv } from "@gajae-code/utils/env";
|
|
2
2
|
|
|
3
3
|
export type EditMode = "replace" | "patch" | "hashline" | "vim" | "apply_patch";
|
|
4
4
|
|
|
@@ -34,7 +34,7 @@ export function resolveEditMode(session: EditModeSessionLike): EditMode {
|
|
|
34
34
|
const modelVariant = session.settings.getEditVariantForModel?.(activeModel);
|
|
35
35
|
if (modelVariant) return modelVariant;
|
|
36
36
|
|
|
37
|
-
const envMode = normalizeEditMode($
|
|
37
|
+
const envMode = normalizeEditMode($pickenv("GJC_EDIT_VARIANT", "PI_EDIT_VARIANT"));
|
|
38
38
|
if (envMode) return envMode;
|
|
39
39
|
|
|
40
40
|
const settingsMode = normalizeEditMode(String(session.settings.get("edit.mode") ?? ""));
|
|
@@ -143,6 +143,7 @@ function tokenizePastedPathCandidates(
|
|
|
143
143
|
text: string,
|
|
144
144
|
options: DecodePastedPathOptions,
|
|
145
145
|
maxCandidates: number,
|
|
146
|
+
requireCompletePayload = false,
|
|
146
147
|
): TokenizePastedPathsResult {
|
|
147
148
|
if (text.length > MAX_PASTED_IMAGE_PASTE_CHARACTERS) return { kind: "invalid" };
|
|
148
149
|
const platform = options.platform ?? process.platform;
|
|
@@ -154,7 +155,9 @@ function tokenizePastedPathCandidates(
|
|
|
154
155
|
|
|
155
156
|
const startCandidate = (): TokenizePastedPathsResult | undefined => {
|
|
156
157
|
if (candidateStarted) return undefined;
|
|
157
|
-
if (candidates.length >= maxCandidates)
|
|
158
|
+
if (!requireCompletePayload && candidates.length >= maxCandidates) {
|
|
159
|
+
return { kind: "too-many", maxCandidates, candidates: [...candidates] };
|
|
160
|
+
}
|
|
158
161
|
candidateStarted = true;
|
|
159
162
|
return undefined;
|
|
160
163
|
};
|
|
@@ -167,7 +170,9 @@ function tokenizePastedPathCandidates(
|
|
|
167
170
|
const finishCandidate = (): TokenizePastedPathsResult | undefined => {
|
|
168
171
|
if (!candidateStarted) return undefined;
|
|
169
172
|
if (!candidate) return { kind: "invalid" };
|
|
170
|
-
if (candidates.length >= maxCandidates)
|
|
173
|
+
if (!requireCompletePayload && candidates.length >= maxCandidates) {
|
|
174
|
+
return { kind: "too-many", maxCandidates, candidates: [...candidates] };
|
|
175
|
+
}
|
|
171
176
|
const decoded = decodeListToken(candidate, options, platform);
|
|
172
177
|
if (decoded === undefined) return { kind: "invalid" };
|
|
173
178
|
candidates.push(decoded);
|
|
@@ -225,6 +230,7 @@ function tokenizePastedPathCandidates(
|
|
|
225
230
|
if (state !== "normal") return { kind: "invalid" };
|
|
226
231
|
const finalResult = finishCandidate();
|
|
227
232
|
if (finalResult) return finalResult;
|
|
233
|
+
if (candidates.length > maxCandidates) return { kind: "too-many", maxCandidates, candidates };
|
|
228
234
|
return candidates.length === 0 ? { kind: "invalid" } : { kind: "candidates", candidates };
|
|
229
235
|
}
|
|
230
236
|
|
|
@@ -257,8 +263,12 @@ function isRecognizedClipboardTempPath(filePath: string, platform: NodeJS.Platfo
|
|
|
257
263
|
/** Resolve one recognized clipboard-temp image path lexically, before any filesystem access. */
|
|
258
264
|
export function resolvePastedImagePath(text: string, options: ResolvePastedImagePathOptions = {}): string | undefined {
|
|
259
265
|
const platform = options.platform ?? process.platform;
|
|
260
|
-
const
|
|
261
|
-
if (
|
|
266
|
+
const terminalNewlinesTrimmed = text.replace(/(?:\r\n|\r|\n)+$/, "");
|
|
267
|
+
if (/\r|\n/.test(terminalNewlinesTrimmed)) return undefined;
|
|
268
|
+
const candidates = decodePastedPathCandidates(terminalNewlinesTrimmed, options, 1);
|
|
269
|
+
if (candidates?.length !== 1) return undefined;
|
|
270
|
+
const candidate = candidates[0];
|
|
271
|
+
if (!IMAGE_FILE_EXTENSION_PATTERN.test(candidate)) return undefined;
|
|
262
272
|
if (platform === "win32" && isRemoteWindowsPath(candidate)) return undefined;
|
|
263
273
|
const resolved = resolveCandidate(candidate, options, platform);
|
|
264
274
|
return isRecognizedClipboardTempPath(resolved, platform) ? resolved : undefined;
|
|
@@ -274,10 +284,9 @@ export function parsePastedImagePaths(
|
|
|
274
284
|
options: ResolvePastedImagePathOptions = {},
|
|
275
285
|
): PastedImagePathParseResult | undefined {
|
|
276
286
|
const platform = options.platform ?? process.platform;
|
|
277
|
-
const tokenized = tokenizePastedPathCandidates(text, options, MAX_PASTED_IMAGE_COUNT);
|
|
287
|
+
const tokenized = tokenizePastedPathCandidates(text, options, MAX_PASTED_IMAGE_COUNT, true);
|
|
278
288
|
if (tokenized.kind === "too-many") {
|
|
279
289
|
if (
|
|
280
|
-
tokenized.candidates.length !== MAX_PASTED_IMAGE_COUNT ||
|
|
281
290
|
tokenized.candidates.some(
|
|
282
291
|
candidate =>
|
|
283
292
|
!IMAGE_FILE_EXTENSION_PATTERN.test(candidate) ||
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Predict the user's likely next prompt after an agent turn using a smol,
|
|
3
|
+
* fast model. Native GJC port of the ghost-text prompt-suggestion behavior
|
|
4
|
+
* from Claude Code: the prediction renders as dim ghost text in the empty
|
|
5
|
+
* composer and Tab accepts it.
|
|
6
|
+
*/
|
|
7
|
+
import type { AgentMessage } from "@gajae-code/agent-core";
|
|
8
|
+
import { type Api, type AssistantMessage, completeSimple, type Model } from "@gajae-code/ai";
|
|
9
|
+
import { logger, prompt } from "@gajae-code/utils";
|
|
10
|
+
import type { ModelRegistry } from "../config/model-registry";
|
|
11
|
+
import { resolveRoleSelection } from "../config/model-resolver";
|
|
12
|
+
import type { Settings } from "../config/settings";
|
|
13
|
+
import suggestionSystemPrompt from "../prompts/system/prompt-suggestion-system.md" with { type: "text" };
|
|
14
|
+
|
|
15
|
+
const SUGGESTION_SYSTEM_PROMPT = prompt.render(suggestionSystemPrompt);
|
|
16
|
+
|
|
17
|
+
export const PROMPT_SUGGESTION_MAX_WORDS = 12;
|
|
18
|
+
export const PROMPT_SUGGESTION_MAX_CHARS = 100;
|
|
19
|
+
|
|
20
|
+
const SUGGESTION_MAX_TOKENS = 64;
|
|
21
|
+
const REASONING_SAFE_MAX_TOKENS = 1024;
|
|
22
|
+
const MAX_CONTEXT_MESSAGES = 12;
|
|
23
|
+
const MAX_MESSAGE_CHARS = 1500;
|
|
24
|
+
const MAX_CONTEXT_CHARS = 8000;
|
|
25
|
+
|
|
26
|
+
/** Single words a user plausibly types on their own; anything else needs >= 2 words. */
|
|
27
|
+
const SINGLE_WORD_ALLOWLIST = new Set([
|
|
28
|
+
"yes",
|
|
29
|
+
"yeah",
|
|
30
|
+
"yep",
|
|
31
|
+
"yea",
|
|
32
|
+
"yup",
|
|
33
|
+
"sure",
|
|
34
|
+
"ok",
|
|
35
|
+
"okay",
|
|
36
|
+
"push",
|
|
37
|
+
"commit",
|
|
38
|
+
"deploy",
|
|
39
|
+
"stop",
|
|
40
|
+
"continue",
|
|
41
|
+
"check",
|
|
42
|
+
"exit",
|
|
43
|
+
"quit",
|
|
44
|
+
"no",
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
/** Model responses that are meta-statements about staying silent, not suggestions. */
|
|
48
|
+
const META_TEXT_PATTERN = /\bsilence is\b|\bstay(s|ing)? silent\b/;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Strip wrapper tags and label prefixes a model sometimes adds despite being
|
|
52
|
+
* told to reply with only the suggestion.
|
|
53
|
+
*/
|
|
54
|
+
export function sanitizePromptSuggestion(raw: string): string {
|
|
55
|
+
return raw
|
|
56
|
+
.trim()
|
|
57
|
+
.replace(/^<(suggestion|response|output|answer|result)>([\s\S]*)<\/\1>$/i, (match, tag: string, inner: string) =>
|
|
58
|
+
// Keep the original when the inner text itself contains the closing
|
|
59
|
+
// tag (nested/ambiguous markup) instead of producing a mangled strip.
|
|
60
|
+
inner.includes(`</${tag.toLowerCase()}>`) || inner.includes(`</${tag.toUpperCase()}>`) ? match : inner,
|
|
61
|
+
)
|
|
62
|
+
.replace(
|
|
63
|
+
/^\s*(suggested\s+(response|reply|input|prompt)|suggestion|response|reply|answer|output|result)\s*:\s*/i,
|
|
64
|
+
"",
|
|
65
|
+
)
|
|
66
|
+
.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Heuristic gate rejecting model output that would make a bad ghost-text
|
|
71
|
+
* suggestion. Returns the rejection reason, or null when the text is usable.
|
|
72
|
+
*/
|
|
73
|
+
export function suppressPromptSuggestionReason(text: string): string | null {
|
|
74
|
+
if (!text) return "empty";
|
|
75
|
+
const lower = text.toLowerCase();
|
|
76
|
+
const wordCount = text.trim().split(/\s+/).length;
|
|
77
|
+
|
|
78
|
+
if (lower === "done") return "done";
|
|
79
|
+
if (
|
|
80
|
+
lower === "nothing found" ||
|
|
81
|
+
lower === "nothing found." ||
|
|
82
|
+
lower.startsWith("nothing to suggest") ||
|
|
83
|
+
lower.startsWith("no suggestion") ||
|
|
84
|
+
META_TEXT_PATTERN.test(lower) ||
|
|
85
|
+
/^\W*silence\W*$/.test(lower)
|
|
86
|
+
) {
|
|
87
|
+
return "meta_text";
|
|
88
|
+
}
|
|
89
|
+
if (/^\(.*\)$|^\[.*\]$/.test(text)) return "meta_wrapped";
|
|
90
|
+
if (
|
|
91
|
+
lower.startsWith("api error:") ||
|
|
92
|
+
lower.startsWith("prompt is too long") ||
|
|
93
|
+
lower.startsWith("request timed out") ||
|
|
94
|
+
lower.startsWith("invalid api key")
|
|
95
|
+
) {
|
|
96
|
+
return "error_message";
|
|
97
|
+
}
|
|
98
|
+
if (/^\w+:\s/.test(text)) return "prefixed_label";
|
|
99
|
+
if (wordCount < 2 && !text.startsWith("/") && !SINGLE_WORD_ALLOWLIST.has(lower)) return "too_few_words";
|
|
100
|
+
if (wordCount > PROMPT_SUGGESTION_MAX_WORDS) return "too_many_words";
|
|
101
|
+
if (text.length >= PROMPT_SUGGESTION_MAX_CHARS) return "too_long";
|
|
102
|
+
if (/[.!?]\s+[A-Z]/.test(text)) return "multiple_sentences";
|
|
103
|
+
if (/[\n*]|\*\*/.test(text)) return "has_formatting";
|
|
104
|
+
if (
|
|
105
|
+
/thanks|thank you|looks good|sounds good|that works|that worked|that's all|nice|great|perfect|makes sense|awesome|excellent/.test(
|
|
106
|
+
lower,
|
|
107
|
+
)
|
|
108
|
+
) {
|
|
109
|
+
return "evaluative";
|
|
110
|
+
}
|
|
111
|
+
if (
|
|
112
|
+
/^(let me|i'll|i've|i'm|i can|i would|i think|i notice|here's|here is|here are|that's|this is|this will|you can|you should|you could|sure,|of course|certainly)/i.test(
|
|
113
|
+
text,
|
|
114
|
+
)
|
|
115
|
+
) {
|
|
116
|
+
return "claude_voice";
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function extractTranscriptEntry(message: AgentMessage): { role: "User" | "Assistant"; text: string } | null {
|
|
122
|
+
if (message.role === "user") {
|
|
123
|
+
const content = message.content;
|
|
124
|
+
const text =
|
|
125
|
+
typeof content === "string"
|
|
126
|
+
? content
|
|
127
|
+
: content
|
|
128
|
+
.filter(block => block.type === "text")
|
|
129
|
+
.map(block => block.text)
|
|
130
|
+
.join("\n");
|
|
131
|
+
const trimmed = text.trim();
|
|
132
|
+
return trimmed ? { role: "User", text: trimmed } : null;
|
|
133
|
+
}
|
|
134
|
+
if (message.role === "assistant") {
|
|
135
|
+
const text = (message as AssistantMessage).content
|
|
136
|
+
.filter(block => block.type === "text")
|
|
137
|
+
.map(block => block.text)
|
|
138
|
+
.join("\n")
|
|
139
|
+
.trim();
|
|
140
|
+
return text ? { role: "Assistant", text } : null;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Build a compact recent-conversation transcript for the prediction request.
|
|
147
|
+
* Returns null when there is no user message to predict from.
|
|
148
|
+
*/
|
|
149
|
+
export function buildPromptSuggestionContext(messages: AgentMessage[]): string | null {
|
|
150
|
+
const entries: { role: "User" | "Assistant"; text: string }[] = [];
|
|
151
|
+
let totalChars = 0;
|
|
152
|
+
for (let index = messages.length - 1; index >= 0 && entries.length < MAX_CONTEXT_MESSAGES; index--) {
|
|
153
|
+
const message = messages[index];
|
|
154
|
+
if (!message) continue;
|
|
155
|
+
const entry = extractTranscriptEntry(message);
|
|
156
|
+
if (!entry) continue;
|
|
157
|
+
const text = entry.text.length > MAX_MESSAGE_CHARS ? `${entry.text.slice(0, MAX_MESSAGE_CHARS)}…` : entry.text;
|
|
158
|
+
if (totalChars + text.length > MAX_CONTEXT_CHARS && entries.length > 0) break;
|
|
159
|
+
entries.push({ role: entry.role, text });
|
|
160
|
+
totalChars += text.length;
|
|
161
|
+
}
|
|
162
|
+
if (!entries.some(entry => entry.role === "User")) return null;
|
|
163
|
+
entries.reverse();
|
|
164
|
+
const transcript = entries.map(entry => `${entry.role}: ${entry.text}`).join("\n\n");
|
|
165
|
+
return `<conversation>\n${transcript}\n</conversation>\n\nPredict the user's next message.`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function getSuggestionModel(
|
|
169
|
+
registry: ModelRegistry,
|
|
170
|
+
settings: Settings,
|
|
171
|
+
currentModel?: Model<Api>,
|
|
172
|
+
): Model<Api> | undefined {
|
|
173
|
+
const availableModels = registry.getAvailable();
|
|
174
|
+
if (availableModels.length === 0) return undefined;
|
|
175
|
+
const model = resolveRoleSelection(["default"], settings, availableModels, registry)?.model;
|
|
176
|
+
if (model) return model;
|
|
177
|
+
return currentModel;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function extractSuggestionText(contentBlocks: AssistantMessage["content"]): string {
|
|
181
|
+
let text = "";
|
|
182
|
+
for (const content of contentBlocks) {
|
|
183
|
+
if (content.type === "text") {
|
|
184
|
+
text += content.text;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return text;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Generate a predicted next user prompt from the recent conversation.
|
|
192
|
+
* Returns null when no usable suggestion is produced (silence is the
|
|
193
|
+
* expected steady state).
|
|
194
|
+
*/
|
|
195
|
+
export async function generatePromptSuggestion(
|
|
196
|
+
messages: AgentMessage[],
|
|
197
|
+
registry: ModelRegistry,
|
|
198
|
+
settings: Settings,
|
|
199
|
+
sessionId?: string,
|
|
200
|
+
currentModel?: Model<Api>,
|
|
201
|
+
metadataResolver?: (provider: string) => Record<string, unknown> | undefined,
|
|
202
|
+
): Promise<string | null> {
|
|
203
|
+
const context = buildPromptSuggestionContext(messages);
|
|
204
|
+
if (!context) return null;
|
|
205
|
+
|
|
206
|
+
const model = getSuggestionModel(registry, settings, currentModel);
|
|
207
|
+
if (!model) {
|
|
208
|
+
logger.debug("prompt-suggestion: no model available");
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const apiKey = await registry.getApiKey(model, sessionId);
|
|
213
|
+
if (!apiKey) {
|
|
214
|
+
logger.debug("prompt-suggestion: no API key", { provider: model.provider, id: model.id });
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
// Resolve metadata after getApiKey so the session-sticky credential for
|
|
218
|
+
// this request is already recorded (same contract as title-generator).
|
|
219
|
+
const metadata = metadataResolver?.(model.provider);
|
|
220
|
+
|
|
221
|
+
// A suggestion is a 2-12 word task, but some reasoning backends ignore
|
|
222
|
+
// disableReasoning; reserve output room for the answer after any
|
|
223
|
+
// unavoidable thinking tokens.
|
|
224
|
+
const maxTokens = model.reasoning
|
|
225
|
+
? Math.max(SUGGESTION_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS)
|
|
226
|
+
: SUGGESTION_MAX_TOKENS;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const response = await completeSimple(
|
|
230
|
+
model,
|
|
231
|
+
{
|
|
232
|
+
systemPrompt: [SUGGESTION_SYSTEM_PROMPT],
|
|
233
|
+
messages: [{ role: "user", content: context, timestamp: Date.now() }],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
apiKey,
|
|
237
|
+
maxTokens,
|
|
238
|
+
disableReasoning: true,
|
|
239
|
+
metadata,
|
|
240
|
+
},
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
if (response.stopReason === "error") {
|
|
244
|
+
logger.debug("prompt-suggestion: response error", {
|
|
245
|
+
model: `${model.provider}/${model.id}`,
|
|
246
|
+
errorMessage: response.errorMessage,
|
|
247
|
+
});
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const suggestion = sanitizePromptSuggestion(extractSuggestionText(response.content));
|
|
252
|
+
const suppressReason = suppressPromptSuggestionReason(suggestion);
|
|
253
|
+
if (suppressReason) {
|
|
254
|
+
logger.debug("prompt-suggestion: suppressed", { reason: suppressReason });
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
return suggestion;
|
|
258
|
+
} catch (err) {
|
|
259
|
+
logger.debug("prompt-suggestion: error", {
|
|
260
|
+
model: `${model.provider}/${model.id}`,
|
|
261
|
+
error: err instanceof Error ? err.message : String(err),
|
|
262
|
+
});
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
}
|