@sayknow-cli/coding-agent 0.6.6 → 0.6.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.
Files changed (65) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/types/config/settings-schema.d.ts +79 -0
  3. package/dist/types/i18n/messages/en.d.ts +2 -0
  4. package/dist/types/modes/components/welcome.d.ts +21 -4
  5. package/dist/types/modes/interactive-mode.d.ts +2 -5
  6. package/dist/types/modes/types.d.ts +2 -0
  7. package/dist/types/sdk/broker/broker.d.ts +22 -0
  8. package/dist/types/sdk/broker/process-guard.d.ts +71 -0
  9. package/dist/types/sdk/broker/transport.d.ts +2 -0
  10. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
  11. package/dist/types/session/agent-session.d.ts +9 -0
  12. package/dist/types/session/auth-storage-discovery.d.ts +15 -0
  13. package/dist/types/session/auto-fallback.d.ts +27 -0
  14. package/dist/types/session/fallback-chain-controller.d.ts +5 -0
  15. package/dist/types/session/response-language.d.ts +25 -0
  16. package/dist/types/setup/model-onboarding-guidance.d.ts +8 -1
  17. package/dist/types/setup/provider-onboarding.d.ts +2 -0
  18. package/dist/types/tools/index.d.ts +1 -0
  19. package/dist/types/tools/locate-core.d.ts +97 -0
  20. package/dist/types/tools/locate.d.ts +44 -0
  21. package/package.json +7 -7
  22. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  23. package/src/cli/setup-cli.ts +7 -4
  24. package/src/commands/sdk.ts +3 -0
  25. package/src/commands/setup.ts +4 -1
  26. package/src/config/settings-schema.ts +82 -0
  27. package/src/decisions/typesafe-backend.ts +38 -4
  28. package/src/i18n/messages/de.settings.ts +26 -0
  29. package/src/i18n/messages/de.ts +2 -0
  30. package/src/i18n/messages/en.ts +2 -0
  31. package/src/i18n/messages/es.settings.ts +26 -0
  32. package/src/i18n/messages/es.ts +2 -0
  33. package/src/i18n/messages/fr.settings.ts +26 -0
  34. package/src/i18n/messages/fr.ts +2 -0
  35. package/src/i18n/messages/ja.settings.ts +26 -0
  36. package/src/i18n/messages/ja.ts +2 -0
  37. package/src/i18n/messages/ko.settings.ts +25 -0
  38. package/src/i18n/messages/ko.ts +2 -0
  39. package/src/i18n/messages/zh.settings.ts +23 -0
  40. package/src/i18n/messages/zh.ts +2 -0
  41. package/src/internal-urls/docs-index.generated.ts +7 -6
  42. package/src/modes/components/welcome.ts +241 -68
  43. package/src/modes/controllers/input-controller.ts +2 -0
  44. package/src/modes/controllers/selector-controller.ts +13 -0
  45. package/src/modes/interactive-mode.ts +123 -14
  46. package/src/modes/types.ts +2 -0
  47. package/src/prompts/system/system-prompt.md +7 -2
  48. package/src/prompts/tools/locate.md +12 -0
  49. package/src/sdk/broker/broker.ts +75 -1
  50. package/src/sdk/broker/process-guard.ts +160 -0
  51. package/src/sdk/broker/transport.ts +15 -1
  52. package/src/sdk/bus/chat-daemon-runtime.ts +13 -1
  53. package/src/sdk/protocol/operation-inventory.generated.json +22 -0
  54. package/src/sdk/session.ts +4 -1
  55. package/src/session/agent-session.ts +118 -2
  56. package/src/session/auth-storage-discovery.ts +22 -7
  57. package/src/session/auto-fallback.ts +59 -0
  58. package/src/session/fallback-chain-controller.ts +5 -0
  59. package/src/session/response-language.ts +71 -0
  60. package/src/setup/model-onboarding-guidance.ts +29 -14
  61. package/src/setup/provider-onboarding.ts +5 -0
  62. package/src/slash-commands/builtin-registry.ts +112 -2
  63. package/src/tools/index.ts +3 -0
  64. package/src/tools/locate-core.ts +720 -0
  65. package/src/tools/locate.ts +203 -0
@@ -0,0 +1,203 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { AgentTool, AgentToolResult } from "@sayknow-cli/agent-core";
4
+ import * as natives from "@sayknow-cli/natives";
5
+ import { prompt, untilAborted } from "@sayknow-cli/utils";
6
+ import * as z from "zod/v4";
7
+ import { createTypeSafeDecisionBackend, TYPESAFE_PROVIDER } from "../decisions/typesafe-backend";
8
+ import locateDescription from "../prompts/tools/locate.md" with { type: "text" };
9
+ import type { ToolSession } from ".";
10
+ import {
11
+ FILE_BAR,
12
+ type LocateResult,
13
+ locateCode,
14
+ type OutlineLine,
15
+ outlineFromSegments,
16
+ outlineFromSource,
17
+ } from "./locate-core";
18
+ import { resolveToCwd } from "./path-utils";
19
+ import { ToolError } from "./tool-errors";
20
+
21
+ const locateSchema = z.object({
22
+ query: z.string().min(3).describe("What the code does, as a question or description"),
23
+ path: z.string().optional().describe("Directory to search (default: working directory)"),
24
+ limit: z.number().int().min(1).max(40).optional().describe("Most files to return (default 12)"),
25
+ });
26
+
27
+ type LocateParams = z.infer<typeof locateSchema>;
28
+
29
+ export interface LocateToolDetails {
30
+ result: Omit<LocateResult, "files" | "weakLeads"> & { files: string[] };
31
+ }
32
+
33
+ /** Never uploaded: dependency, lock, build and binary files add cost without signal. */
34
+ const SKIPPED_EXTENSIONS = new Set([
35
+ ".png",
36
+ ".jpg",
37
+ ".jpeg",
38
+ ".gif",
39
+ ".webp",
40
+ ".ico",
41
+ ".icns",
42
+ ".svg",
43
+ ".pdf",
44
+ ".zip",
45
+ ".gz",
46
+ ".tgz",
47
+ ".xz",
48
+ ".woff",
49
+ ".woff2",
50
+ ".ttf",
51
+ ".otf",
52
+ ".mp3",
53
+ ".mp4",
54
+ ".mov",
55
+ ".wav",
56
+ ".wasm",
57
+ ".node",
58
+ ".dylib",
59
+ ".so",
60
+ ".dll",
61
+ ".exe",
62
+ ".bin",
63
+ ".dmg",
64
+ ".db",
65
+ ".sqlite",
66
+ ".map",
67
+ ".lock",
68
+ ".snap",
69
+ ".pile",
70
+ ]);
71
+ const SKIPPED_NAMES = new Set([
72
+ "package-lock.json",
73
+ "bun.lock",
74
+ "bun.lockb",
75
+ "yarn.lock",
76
+ "pnpm-lock.yaml",
77
+ "Cargo.lock",
78
+ ]);
79
+ const MAX_LISTED_FILES = 20_000;
80
+ const MAX_OUTLINE_BYTES = 512 * 1024;
81
+
82
+ export class LocateTool implements AgentTool<typeof locateSchema, LocateToolDetails> {
83
+ readonly name = "locate";
84
+ readonly label = "Locate";
85
+ readonly summary = "Find code by describing what it does (Jev relevance judgement)";
86
+ /**
87
+ * Essential when offered: its value is being picked instead of a grep, which a
88
+ * discoverable tool hidden behind search_tool_bm25 would rarely be. createIf already
89
+ * keeps it out unless a TypeSafe key is stored.
90
+ */
91
+ readonly loadMode = "essential";
92
+ readonly description: string;
93
+ readonly parameters = locateSchema;
94
+ readonly strict = true;
95
+
96
+ /**
97
+ * Offered only when it can work: a TypeSafe key is stored and `locate.enabled` is on.
98
+ * Without the hosted model every judgement would fall to an uncalibrated LLM, one
99
+ * request per folder batch, which costs more than the search saves.
100
+ */
101
+ static createIf(session: ToolSession): LocateTool | null {
102
+ if (!session.settings.get("locate.enabled")) return null;
103
+ if (!session.modelRegistry?.authStorage.hasAuth(TYPESAFE_PROVIDER)) return null;
104
+ return new LocateTool(session);
105
+ }
106
+
107
+ constructor(private readonly session: ToolSession) {
108
+ this.description = prompt.render(locateDescription);
109
+ }
110
+
111
+ async execute(
112
+ _toolCallId: string,
113
+ params: LocateParams,
114
+ signal?: AbortSignal,
115
+ ): Promise<AgentToolResult<LocateToolDetails>> {
116
+ return untilAborted(signal, async () => {
117
+ const registry = this.session.modelRegistry;
118
+ if (!registry) throw new ToolError("locate needs a model registry");
119
+ const root = resolveToCwd(params.path ?? ".", this.session.cwd);
120
+ if (!fs.statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
121
+ throw new ToolError(`Not a directory: ${params.path ?? root}`);
122
+ }
123
+ if (root === path.parse(root).root) throw new ToolError("Searching from the filesystem root is not allowed");
124
+
125
+ const backend = createTypeSafeDecisionBackend({
126
+ registry,
127
+ sessionId: this.session.getSessionId?.() ?? undefined,
128
+ });
129
+ const result = await locateCode(
130
+ { query: params.query, root, limit: params.limit ?? 12 },
131
+ { listFiles, outline, readText, decide: request => backend.decide(request), signal },
132
+ );
133
+ if (result.requests > 0 && result.failedRequests === result.requests) {
134
+ throw new ToolError(
135
+ "Jev did not answer (TypeSafe key missing, rejected, or unreachable). Use search/find, or re-add the key with /provider typesafe.",
136
+ );
137
+ }
138
+ return {
139
+ content: [{ type: "text", text: formatLocateResult(params.query, root, this.session.cwd, result) }],
140
+ details: { result: { ...result, files: result.files.map(file => file.path) } },
141
+ };
142
+ });
143
+ }
144
+ }
145
+
146
+ async function listFiles(root: string, signal?: AbortSignal): Promise<string[]> {
147
+ const { matches } = await natives.glob({
148
+ pattern: "**/*",
149
+ path: root,
150
+ fileType: natives.FileType.File,
151
+ hidden: false,
152
+ gitignore: true,
153
+ maxResults: MAX_LISTED_FILES,
154
+ signal,
155
+ });
156
+ return matches
157
+ .map(match => match.path.split(path.sep).join("/"))
158
+ .filter(file => {
159
+ const base = path.posix.basename(file);
160
+ return !SKIPPED_NAMES.has(base) && !SKIPPED_EXTENSIONS.has(path.posix.extname(base).toLowerCase());
161
+ });
162
+ }
163
+
164
+ /** Local-only text for the keyword score; never sent anywhere. */
165
+ async function readText(absolutePath: string): Promise<string | undefined> {
166
+ const stat = fs.statSync(absolutePath, { throwIfNoEntry: false });
167
+ if (!stat?.isFile() || stat.size > MAX_OUTLINE_BYTES) return undefined;
168
+ return Bun.file(absolutePath).text();
169
+ }
170
+
171
+ async function outline(absolutePath: string): Promise<OutlineLine[]> {
172
+ const stat = fs.statSync(absolutePath, { throwIfNoEntry: false });
173
+ if (!stat?.isFile() || stat.size > MAX_OUTLINE_BYTES) return [];
174
+ const code = await Bun.file(absolutePath).text();
175
+ // minBodyLines/minCommentLines 1: every body and comment is elided, signatures remain.
176
+ const summary = natives.summarizeCode({ code, path: absolutePath, minBodyLines: 1, minCommentLines: 1 });
177
+ const lines = code.split("\n");
178
+ return summary.parsed ? outlineFromSegments(summary.segments, lines) : outlineFromSource(lines);
179
+ }
180
+
181
+ export function formatLocateResult(query: string, root: string, cwd: string, result: LocateResult): string {
182
+ const where = path.relative(cwd, root) || ".";
183
+ const lines = [
184
+ `locate "${query}" in ${where}`,
185
+ `${result.files.length} relevant file(s); judged ${result.foldersJudged} folders and ${result.filesJudged} of ${result.candidates} candidate files in ${result.requests} Jev requests, ${(result.durationMs / 1000).toFixed(1)}s. Sent: paths and declaration lines only.`,
186
+ ];
187
+ if (result.failedRequests > 0) lines.push(`${result.failedRequests} request(s) failed; results may be incomplete.`);
188
+ if (result.truncated) lines.push("Search budget reached; narrow `path` for a complete pass.");
189
+ const show = (files: LocateResult["files"]) => {
190
+ files.forEach((file, index) => {
191
+ const shown = path.relative(cwd, path.join(root, file.path));
192
+ lines.push("", `${index + 1}. ${shown} (${file.score.toFixed(2)}${file.weak ? ", weak" : ""})`);
193
+ for (const entry of file.outline.slice(0, 12)) lines.push(` L${entry.line} ${entry.text}`);
194
+ if (file.outline.length > 12) lines.push(` … ${file.outline.length - 12} more declarations`);
195
+ });
196
+ };
197
+ if (result.files.length > 0) show(result.files);
198
+ else if (result.weakLeads.length > 0) {
199
+ lines.push("", `No file cleared the relevance bar (${FILE_BAR}). Weak leads:`);
200
+ show(result.weakLeads);
201
+ } else lines.push("", "No candidates. Try a broader path or search/find.");
202
+ return lines.join("\n");
203
+ }