@oh-my-pi/pi-coding-agent 4.1.0 → 4.2.0
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 +40 -0
- package/README.md +2 -1
- package/docs/sdk.md +0 -3
- package/package.json +6 -5
- package/src/config.ts +9 -0
- package/src/core/agent-storage.ts +450 -0
- package/src/core/auth-storage.ts +102 -183
- package/src/core/compaction/branch-summarization.ts +5 -4
- package/src/core/compaction/compaction.ts +7 -6
- package/src/core/compaction/utils.ts +6 -11
- package/src/core/custom-commands/bundled/review/index.ts +22 -94
- package/src/core/custom-share.ts +66 -0
- package/src/core/history-storage.ts +15 -7
- package/src/core/prompt-templates.ts +271 -1
- package/src/core/sdk.ts +14 -3
- package/src/core/settings-manager.ts +100 -34
- package/src/core/slash-commands.ts +4 -1
- package/src/core/storage-migration.ts +215 -0
- package/src/core/system-prompt.ts +87 -289
- package/src/core/title-generator.ts +3 -2
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +2 -1
- package/src/core/tools/calculator.ts +2 -1
- package/src/core/tools/edit.ts +2 -1
- package/src/core/tools/find.ts +2 -1
- package/src/core/tools/gemini-image.ts +2 -1
- package/src/core/tools/git.ts +2 -2
- package/src/core/tools/grep.ts +2 -1
- package/src/core/tools/index.test.ts +0 -28
- package/src/core/tools/index.ts +0 -6
- package/src/core/tools/lsp/index.ts +2 -1
- package/src/core/tools/output.ts +2 -1
- package/src/core/tools/read.ts +4 -1
- package/src/core/tools/ssh.ts +4 -2
- package/src/core/tools/task/agents.ts +56 -30
- package/src/core/tools/task/commands.ts +9 -8
- package/src/core/tools/task/index.ts +7 -15
- package/src/core/tools/web-fetch.ts +2 -1
- package/src/core/tools/web-search/auth.ts +106 -16
- package/src/core/tools/web-search/index.ts +3 -2
- package/src/core/tools/web-search/providers/anthropic.ts +44 -6
- package/src/core/tools/write.ts +2 -1
- package/src/core/voice.ts +3 -1
- package/src/main.ts +1 -1
- package/src/migrations.ts +20 -20
- package/src/modes/interactive/controllers/command-controller.ts +527 -0
- package/src/modes/interactive/controllers/event-controller.ts +340 -0
- package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
- package/src/modes/interactive/controllers/input-controller.ts +585 -0
- package/src/modes/interactive/controllers/selector-controller.ts +585 -0
- package/src/modes/interactive/interactive-mode.ts +364 -3143
- package/src/modes/interactive/theme/theme.ts +5 -5
- package/src/modes/interactive/types.ts +189 -0
- package/src/modes/interactive/utils/ui-helpers.ts +449 -0
- package/src/modes/interactive/utils/voice-manager.ts +96 -0
- package/src/prompts/{explore.md → agents/explore.md} +7 -5
- package/src/prompts/agents/frontmatter.md +7 -0
- package/src/prompts/{plan.md → agents/plan.md} +3 -3
- package/src/prompts/{task.md → agents/task.md} +1 -1
- package/src/prompts/review-request.md +44 -8
- package/src/prompts/system/custom-system-prompt.md +80 -0
- package/src/prompts/system/file-operations.md +12 -0
- package/src/prompts/system/system-prompt.md +232 -0
- package/src/prompts/system/title-system.md +2 -0
- package/src/prompts/tools/bash.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +9 -3
- package/src/core/tools/rulebook.ts +0 -132
- package/src/prompts/system-prompt.md +0 -43
- package/src/prompts/title-system.md +0 -8
- /package/src/prompts/{architect-plan.md → agents/architect-plan.md} +0 -0
- /package/src/prompts/{implement-with-critic.md → agents/implement-with-critic.md} +0 -0
- /package/src/prompts/{implement.md → agents/implement.md} +0 -0
- /package/src/prompts/{init.md → agents/init.md} +0 -0
- /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
- /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
- /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
- /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
- /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
- /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
- /package/src/prompts/{summarization-system.md → system/summarization-system.md} +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom share script loader.
|
|
3
|
+
*
|
|
4
|
+
* Allows users to define a custom share handler at ~/.omp/agent/share.ts
|
|
5
|
+
* that will be used instead of the default GitHub Gist sharing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { getAgentDir } from "../config";
|
|
11
|
+
|
|
12
|
+
export interface CustomShareResult {
|
|
13
|
+
/** URL to display/open (optional - script may handle everything itself) */
|
|
14
|
+
url?: string;
|
|
15
|
+
/** Additional message to show the user */
|
|
16
|
+
message?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type CustomShareFn = (htmlPath: string) => Promise<CustomShareResult | string | undefined>;
|
|
20
|
+
|
|
21
|
+
interface LoadedCustomShare {
|
|
22
|
+
path: string;
|
|
23
|
+
fn: CustomShareFn;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SHARE_SCRIPT_CANDIDATES = ["share.ts", "share.js", "share.mjs"];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the path to the custom share script if it exists.
|
|
30
|
+
*/
|
|
31
|
+
export function getCustomSharePath(): string | null {
|
|
32
|
+
const agentDir = getAgentDir();
|
|
33
|
+
|
|
34
|
+
for (const candidate of SHARE_SCRIPT_CANDIDATES) {
|
|
35
|
+
const scriptPath = join(agentDir, candidate);
|
|
36
|
+
if (existsSync(scriptPath)) {
|
|
37
|
+
return scriptPath;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load the custom share script if it exists.
|
|
46
|
+
*/
|
|
47
|
+
export async function loadCustomShare(): Promise<LoadedCustomShare | null> {
|
|
48
|
+
const scriptPath = getCustomSharePath();
|
|
49
|
+
if (!scriptPath) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const module = await import(scriptPath);
|
|
55
|
+
const fn = module.default;
|
|
56
|
+
|
|
57
|
+
if (typeof fn !== "function") {
|
|
58
|
+
throw new Error("share script must export a default function");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { path: scriptPath, fn };
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
64
|
+
throw new Error(`Failed to load share script: ${message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -29,6 +29,9 @@ export class HistoryStorage {
|
|
|
29
29
|
private searchStmt: Statement;
|
|
30
30
|
private lastPromptStmt: Statement;
|
|
31
31
|
|
|
32
|
+
// In-memory cache of last prompt to avoid sync DB reads on add
|
|
33
|
+
private lastPromptCache: string | null = null;
|
|
34
|
+
|
|
32
35
|
private constructor(dbPath: string) {
|
|
33
36
|
this.ensureDir(dbPath);
|
|
34
37
|
|
|
@@ -71,6 +74,9 @@ END;
|
|
|
71
74
|
"SELECT h.id, h.prompt, h.created_at, h.cwd FROM history_fts f JOIN history h ON h.id = f.rowid WHERE history_fts MATCH ? ORDER BY h.created_at DESC, h.id DESC LIMIT ?",
|
|
72
75
|
);
|
|
73
76
|
this.lastPromptStmt = this.db.prepare("SELECT prompt FROM history ORDER BY id DESC LIMIT 1");
|
|
77
|
+
|
|
78
|
+
const last = this.lastPromptStmt.get() as { prompt?: string } | undefined;
|
|
79
|
+
this.lastPromptCache = last?.prompt ?? null;
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
static open(dbPath: string = join(getAgentDir(), "history.db")): HistoryStorage {
|
|
@@ -83,15 +89,17 @@ END;
|
|
|
83
89
|
add(prompt: string, cwd?: string): void {
|
|
84
90
|
const trimmed = prompt.trim();
|
|
85
91
|
if (!trimmed) return;
|
|
92
|
+
if (this.lastPromptCache === trimmed) return;
|
|
86
93
|
|
|
87
|
-
|
|
88
|
-
const last = this.lastPromptStmt.get() as { prompt?: string } | undefined;
|
|
89
|
-
if (last?.prompt === trimmed) return;
|
|
94
|
+
this.lastPromptCache = trimmed;
|
|
90
95
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
setImmediate(() => {
|
|
97
|
+
try {
|
|
98
|
+
this.insertStmt.run(trimmed, cwd ?? null);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logger.error("HistoryStorage add failed", { error: String(error) });
|
|
101
|
+
}
|
|
102
|
+
});
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
getRecent(limit: number): HistoryEntry[] {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
|
+
import Handlebars from "handlebars";
|
|
2
3
|
import { CONFIG_DIR_NAME, getPromptsDir } from "../config";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -11,6 +12,273 @@ export interface PromptTemplate {
|
|
|
11
12
|
source: string; // e.g., "(user)", "(project)", "(project:frontend)"
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export interface TemplateContext extends Record<string, unknown> {
|
|
16
|
+
args?: string[];
|
|
17
|
+
ARGUMENTS?: string;
|
|
18
|
+
arguments?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const handlebars = Handlebars.create();
|
|
22
|
+
|
|
23
|
+
handlebars.registerHelper("arg", function (this: TemplateContext, index: number | string): string {
|
|
24
|
+
const args = this.args ?? [];
|
|
25
|
+
const parsedIndex = typeof index === "number" ? index : Number.parseInt(index, 10);
|
|
26
|
+
if (!Number.isFinite(parsedIndex)) return "";
|
|
27
|
+
const zeroBased = parsedIndex - 1;
|
|
28
|
+
if (zeroBased < 0) return "";
|
|
29
|
+
return args[zeroBased] ?? "";
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* {{#list items prefix="- " suffix="" join="\n"}}{{this}}{{/list}}
|
|
34
|
+
* Renders an array with customizable prefix, suffix, and join separator.
|
|
35
|
+
* Note: Use \n in join for newlines (will be unescaped automatically).
|
|
36
|
+
*/
|
|
37
|
+
handlebars.registerHelper(
|
|
38
|
+
"list",
|
|
39
|
+
function (this: unknown, context: unknown[], options: Handlebars.HelperOptions): string {
|
|
40
|
+
if (!Array.isArray(context) || context.length === 0) return "";
|
|
41
|
+
const prefix = (options.hash.prefix as string) ?? "";
|
|
42
|
+
const suffix = (options.hash.suffix as string) ?? "";
|
|
43
|
+
const rawSeparator = (options.hash.join as string) ?? "\n";
|
|
44
|
+
const separator = rawSeparator.replace(/\\n/g, "\n").replace(/\\t/g, "\t");
|
|
45
|
+
return context.map((item) => `${prefix}${options.fn(item)}${suffix}`).join(separator);
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* {{join array ", "}}
|
|
51
|
+
* Joins an array with a separator (default: ", ").
|
|
52
|
+
*/
|
|
53
|
+
handlebars.registerHelper("join", (context: unknown[], separator?: unknown): string => {
|
|
54
|
+
if (!Array.isArray(context)) return "";
|
|
55
|
+
const sep = typeof separator === "string" ? separator : ", ";
|
|
56
|
+
return context.join(sep);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* {{default value "fallback"}}
|
|
61
|
+
* Returns the value if truthy, otherwise returns the fallback.
|
|
62
|
+
*/
|
|
63
|
+
handlebars.registerHelper("default", (value: unknown, defaultValue: unknown): unknown => value || defaultValue);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* {{pluralize count "item" "items"}}
|
|
67
|
+
* Returns "1 item" or "5 items" based on count.
|
|
68
|
+
*/
|
|
69
|
+
handlebars.registerHelper(
|
|
70
|
+
"pluralize",
|
|
71
|
+
(count: number, singular: string, plural: string): string => `${count} ${count === 1 ? singular : plural}`,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* {{#when value "==" compare}}...{{else}}...{{/when}}
|
|
76
|
+
* Conditional block with comparison operators: ==, ===, !=, !==, >, <, >=, <=
|
|
77
|
+
*/
|
|
78
|
+
handlebars.registerHelper(
|
|
79
|
+
"when",
|
|
80
|
+
function (this: unknown, lhs: unknown, operator: string, rhs: unknown, options: Handlebars.HelperOptions): string {
|
|
81
|
+
const ops: Record<string, (a: unknown, b: unknown) => boolean> = {
|
|
82
|
+
"==": (a, b) => a === b,
|
|
83
|
+
"===": (a, b) => a === b,
|
|
84
|
+
"!=": (a, b) => a !== b,
|
|
85
|
+
"!==": (a, b) => a !== b,
|
|
86
|
+
">": (a, b) => (a as number) > (b as number),
|
|
87
|
+
"<": (a, b) => (a as number) < (b as number),
|
|
88
|
+
">=": (a, b) => (a as number) >= (b as number),
|
|
89
|
+
"<=": (a, b) => (a as number) <= (b as number),
|
|
90
|
+
};
|
|
91
|
+
const fn = ops[operator];
|
|
92
|
+
if (!fn) return options.inverse(this);
|
|
93
|
+
return fn(lhs, rhs) ? options.fn(this) : options.inverse(this);
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* {{#ifAny a b c}}...{{else}}...{{/ifAny}}
|
|
99
|
+
* True if any argument is truthy.
|
|
100
|
+
*/
|
|
101
|
+
handlebars.registerHelper("ifAny", function (this: unknown, ...args: unknown[]): string {
|
|
102
|
+
const options = args.pop() as Handlebars.HelperOptions;
|
|
103
|
+
return args.some(Boolean) ? options.fn(this) : options.inverse(this);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* {{#ifAll a b c}}...{{else}}...{{/ifAll}}
|
|
108
|
+
* True if all arguments are truthy.
|
|
109
|
+
*/
|
|
110
|
+
handlebars.registerHelper("ifAll", function (this: unknown, ...args: unknown[]): string {
|
|
111
|
+
const options = args.pop() as Handlebars.HelperOptions;
|
|
112
|
+
return args.every(Boolean) ? options.fn(this) : options.inverse(this);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* {{#table rows headers="Col1|Col2"}}{{col1}}|{{col2}}{{/table}}
|
|
117
|
+
* Generates a markdown table from an array of objects.
|
|
118
|
+
*/
|
|
119
|
+
handlebars.registerHelper(
|
|
120
|
+
"table",
|
|
121
|
+
function (this: unknown, context: unknown[], options: Handlebars.HelperOptions): string {
|
|
122
|
+
if (!Array.isArray(context) || context.length === 0) return "";
|
|
123
|
+
const headersStr = options.hash.headers as string | undefined;
|
|
124
|
+
const headers = headersStr?.split("|") ?? [];
|
|
125
|
+
const separator = headers.map(() => "---").join(" | ");
|
|
126
|
+
const headerRow = headers.length > 0 ? `| ${headers.join(" | ")} |\n| ${separator} |\n` : "";
|
|
127
|
+
const rows = context.map((item) => `| ${options.fn(item).trim()} |`).join("\n");
|
|
128
|
+
return headerRow + rows;
|
|
129
|
+
},
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* {{#codeblock lang="diff"}}...{{/codeblock}}
|
|
134
|
+
* Wraps content in a fenced code block.
|
|
135
|
+
*/
|
|
136
|
+
handlebars.registerHelper("codeblock", function (this: unknown, options: Handlebars.HelperOptions): string {
|
|
137
|
+
const lang = (options.hash.lang as string) ?? "";
|
|
138
|
+
const content = options.fn(this).trim();
|
|
139
|
+
return `\`\`\`${lang}\n${content}\n\`\`\``;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* {{#xml "tag"}}content{{/xml}}
|
|
144
|
+
* Wraps content in XML-style tags. Returns empty string if content is empty.
|
|
145
|
+
*/
|
|
146
|
+
handlebars.registerHelper("xml", function (this: unknown, tag: string, options: Handlebars.HelperOptions): string {
|
|
147
|
+
const content = options.fn(this).trim();
|
|
148
|
+
if (!content) return "";
|
|
149
|
+
return `<${tag}>\n${content}\n</${tag}>`;
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* {{escapeXml value}}
|
|
154
|
+
* Escapes XML special characters: & < > "
|
|
155
|
+
*/
|
|
156
|
+
handlebars.registerHelper("escapeXml", (value: unknown): string => {
|
|
157
|
+
if (value == null) return "";
|
|
158
|
+
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* {{len array}}
|
|
163
|
+
* Returns the length of an array or string.
|
|
164
|
+
*/
|
|
165
|
+
handlebars.registerHelper("len", (value: unknown): number => {
|
|
166
|
+
if (Array.isArray(value)) return value.length;
|
|
167
|
+
if (typeof value === "string") return value.length;
|
|
168
|
+
return 0;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* {{add a b}}
|
|
173
|
+
* Adds two numbers.
|
|
174
|
+
*/
|
|
175
|
+
handlebars.registerHelper("add", (a: number, b: number): number => (a ?? 0) + (b ?? 0));
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* {{sub a b}}
|
|
179
|
+
* Subtracts b from a.
|
|
180
|
+
*/
|
|
181
|
+
handlebars.registerHelper("sub", (a: number, b: number): number => (a ?? 0) - (b ?? 0));
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* {{#has collection item}}...{{else}}...{{/has}}
|
|
185
|
+
* Checks if an array includes an item or if a Set/Map has a key.
|
|
186
|
+
*/
|
|
187
|
+
handlebars.registerHelper(
|
|
188
|
+
"has",
|
|
189
|
+
function (this: unknown, collection: unknown, item: unknown, options: Handlebars.HelperOptions): string {
|
|
190
|
+
let found = false;
|
|
191
|
+
if (Array.isArray(collection)) {
|
|
192
|
+
found = collection.includes(item);
|
|
193
|
+
} else if (collection instanceof Set) {
|
|
194
|
+
found = collection.has(item);
|
|
195
|
+
} else if (collection instanceof Map) {
|
|
196
|
+
found = collection.has(item);
|
|
197
|
+
} else if (collection && typeof collection === "object") {
|
|
198
|
+
if (typeof item === "string" || typeof item === "number" || typeof item === "symbol") {
|
|
199
|
+
found = item in collection;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return found ? options.fn(this) : options.inverse(this);
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* {{includes array item}}
|
|
208
|
+
* Returns true if array includes item. For use in other helpers.
|
|
209
|
+
*/
|
|
210
|
+
handlebars.registerHelper("includes", (collection: unknown, item: unknown): boolean => {
|
|
211
|
+
if (Array.isArray(collection)) return collection.includes(item);
|
|
212
|
+
if (collection instanceof Set) return collection.has(item);
|
|
213
|
+
if (collection instanceof Map) return collection.has(item);
|
|
214
|
+
return false;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* {{not value}}
|
|
219
|
+
* Returns logical NOT of value. For use in subexpressions.
|
|
220
|
+
*/
|
|
221
|
+
handlebars.registerHelper("not", (value: unknown): boolean => !value);
|
|
222
|
+
|
|
223
|
+
export function renderPromptTemplate(template: string, context: TemplateContext = {}): string {
|
|
224
|
+
const compiled = handlebars.compile(template, { noEscape: true, strict: false });
|
|
225
|
+
const rendered = compiled(context ?? {});
|
|
226
|
+
return optimizePromptLayout(rendered);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function optimizePromptLayout(input: string): string {
|
|
230
|
+
// 1) strip CR / normalize line endings
|
|
231
|
+
let s = input.replace(/\r\n?/g, "\n");
|
|
232
|
+
|
|
233
|
+
// normalize NBSP -> space
|
|
234
|
+
s = s.replace(/\u00A0/g, " ");
|
|
235
|
+
|
|
236
|
+
const lines = s.split("\n").map((line) => {
|
|
237
|
+
// 2) remove trailing whitespace (spaces/tabs) per line
|
|
238
|
+
let l = line.replace(/[ \t]+$/g, "");
|
|
239
|
+
|
|
240
|
+
// 3) lines with only whitespace -> empty line
|
|
241
|
+
if (/^[ \t]*$/.test(l)) return "";
|
|
242
|
+
|
|
243
|
+
// 4) normalize leading indentation: every 2 spaces -> \t (preserve leftover 1 space)
|
|
244
|
+
// NOTE: This is intentionally *only* leading indentation to avoid mangling prose.
|
|
245
|
+
const m = l.match(/^[ \t]+/);
|
|
246
|
+
if (m) {
|
|
247
|
+
const indent = m[0];
|
|
248
|
+
const rest = l.slice(indent.length);
|
|
249
|
+
|
|
250
|
+
let out = "";
|
|
251
|
+
let spaces = 0;
|
|
252
|
+
|
|
253
|
+
for (const ch of indent) {
|
|
254
|
+
if (ch === "\t") {
|
|
255
|
+
// flush pending spaces before existing tab
|
|
256
|
+
out += "\t".repeat(Math.floor(spaces / 2));
|
|
257
|
+
if (spaces % 2) out += " ";
|
|
258
|
+
spaces = 0;
|
|
259
|
+
out += "\t";
|
|
260
|
+
} else {
|
|
261
|
+
spaces++;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
out += "\t".repeat(Math.floor(spaces / 2));
|
|
266
|
+
if (spaces % 2) out += " ";
|
|
267
|
+
|
|
268
|
+
l = out + rest;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return l;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
s = lines.join("\n");
|
|
275
|
+
|
|
276
|
+
// 5) collapse excessive blank lines
|
|
277
|
+
s = s.replace(/\n{3,}/g, "\n\n");
|
|
278
|
+
|
|
279
|
+
return s.trim();
|
|
280
|
+
}
|
|
281
|
+
|
|
14
282
|
/**
|
|
15
283
|
* Parse YAML frontmatter from markdown content
|
|
16
284
|
* Returns { frontmatter, content } where content has frontmatter stripped
|
|
@@ -235,7 +503,9 @@ export function expandPromptTemplate(text: string, templates: PromptTemplate[]):
|
|
|
235
503
|
const template = templates.find((t) => t.name === templateName);
|
|
236
504
|
if (template) {
|
|
237
505
|
const args = parseCommandArgs(argsString);
|
|
238
|
-
|
|
506
|
+
const argsText = args.join(" ");
|
|
507
|
+
const substituted = substituteArgs(template.content, args);
|
|
508
|
+
return renderPromptTemplate(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
|
|
239
509
|
}
|
|
240
510
|
|
|
241
511
|
return text;
|
package/src/core/sdk.ts
CHANGED
|
@@ -71,6 +71,7 @@ import { loadSkills as loadSkillsInternal, type Skill, type SkillWarning } from
|
|
|
71
71
|
import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./slash-commands";
|
|
72
72
|
import { closeAllConnections } from "./ssh/connection-manager";
|
|
73
73
|
import { unmountAll } from "./ssh/sshfs-mount";
|
|
74
|
+
import { migrateJsonStorage } from "./storage-migration";
|
|
74
75
|
import {
|
|
75
76
|
buildSystemPrompt as buildSystemPromptInternal,
|
|
76
77
|
loadProjectContextFiles as loadContextFilesInternal,
|
|
@@ -90,7 +91,6 @@ import {
|
|
|
90
91
|
createSshTool,
|
|
91
92
|
createTools,
|
|
92
93
|
createWriteTool,
|
|
93
|
-
filterRulebookRules,
|
|
94
94
|
getWebSearchTools,
|
|
95
95
|
setPreferredImageProvider,
|
|
96
96
|
setPreferredWebSearchProvider,
|
|
@@ -242,6 +242,13 @@ export async function discoverAuthStorage(agentDir: string = getDefaultAgentDir(
|
|
|
242
242
|
|
|
243
243
|
logger.debug("discoverAuthStorage", { agentDir, primaryPath, allPaths, fallbackPaths });
|
|
244
244
|
|
|
245
|
+
// Migrate legacy JSON files (settings.json, auth.json) to SQLite before loading
|
|
246
|
+
await migrateJsonStorage({
|
|
247
|
+
agentDir,
|
|
248
|
+
settingsPath: join(agentDir, "settings.json"),
|
|
249
|
+
authPaths: [primaryPath, ...fallbackPaths],
|
|
250
|
+
});
|
|
251
|
+
|
|
245
252
|
const storage = new AuthStorage(primaryPath, fallbackPaths);
|
|
246
253
|
await storage.reload();
|
|
247
254
|
return storage;
|
|
@@ -646,7 +653,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
646
653
|
time("discoverTtsrRules");
|
|
647
654
|
|
|
648
655
|
// Filter rules for the rulebook (non-TTSR, non-alwaysApply, with descriptions)
|
|
649
|
-
const rulebookRules =
|
|
656
|
+
const rulebookRules = rulesResult.items.filter((rule) => {
|
|
657
|
+
if (rule.ttsrTrigger) return false;
|
|
658
|
+
if (rule.alwaysApply) return false;
|
|
659
|
+
if (!rule.description) return false;
|
|
660
|
+
return true;
|
|
661
|
+
});
|
|
650
662
|
time("filterRulebookRules");
|
|
651
663
|
|
|
652
664
|
const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
|
|
@@ -658,7 +670,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
658
670
|
const toolSession: ToolSession = {
|
|
659
671
|
cwd,
|
|
660
672
|
hasUI: options.hasUI ?? false,
|
|
661
|
-
rulebookRules,
|
|
662
673
|
eventBus,
|
|
663
674
|
outputSchema: options.outputSchema,
|
|
664
675
|
requireCompleteTool: options.requireCompleteTool,
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, renameSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import { type Settings as SettingsItem, settingsCapability } from "../capability/settings";
|
|
4
|
-
import { getAgentDir } from "../config";
|
|
4
|
+
import { getAgentDbPath, getAgentDir } from "../config";
|
|
5
5
|
import { loadSync } from "../discovery";
|
|
6
6
|
import type { SymbolPreset } from "../modes/interactive/theme/theme";
|
|
7
|
+
import { AgentStorage } from "./agent-storage";
|
|
8
|
+
import { logger } from "./logger";
|
|
7
9
|
|
|
8
10
|
export interface CompactionSettings {
|
|
9
11
|
enabled?: boolean; // default: true
|
|
@@ -337,10 +339,36 @@ function normalizeBashInterceptorSettings(
|
|
|
337
339
|
return { enabled, simpleLs, patterns };
|
|
338
340
|
}
|
|
339
341
|
|
|
342
|
+
let cachedNerdFonts: boolean | null = null;
|
|
343
|
+
|
|
344
|
+
function hasNerdFonts(): boolean {
|
|
345
|
+
if (cachedNerdFonts !== null) {
|
|
346
|
+
return cachedNerdFonts;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const envOverride = process.env.NERD_FONTS;
|
|
350
|
+
if (envOverride === "1") {
|
|
351
|
+
cachedNerdFonts = true;
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
if (envOverride === "0") {
|
|
355
|
+
cachedNerdFonts = false;
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const termProgram = (process.env.TERM_PROGRAM || "").toLowerCase();
|
|
360
|
+
const term = (process.env.TERM || "").toLowerCase();
|
|
361
|
+
const nerdTerms = ["iterm", "wezterm", "kitty", "ghostty", "alacritty"];
|
|
362
|
+
cachedNerdFonts = nerdTerms.some((candidate) => termProgram.includes(candidate) || term.includes(candidate));
|
|
363
|
+
return cachedNerdFonts;
|
|
364
|
+
}
|
|
365
|
+
|
|
340
366
|
function normalizeSettings(settings: Settings): Settings {
|
|
341
367
|
const merged = deepMergeSettings(DEFAULT_SETTINGS, settings);
|
|
368
|
+
const symbolPreset = merged.symbolPreset ?? (hasNerdFonts() ? "nerd" : "unicode");
|
|
342
369
|
return {
|
|
343
370
|
...merged,
|
|
371
|
+
symbolPreset,
|
|
344
372
|
bashInterceptor: normalizeBashInterceptorSettings(merged.bashInterceptor),
|
|
345
373
|
};
|
|
346
374
|
}
|
|
@@ -377,15 +405,23 @@ function deepMergeSettings(base: Settings, overrides: Settings): Settings {
|
|
|
377
405
|
}
|
|
378
406
|
|
|
379
407
|
export class SettingsManager {
|
|
380
|
-
|
|
408
|
+
/** SQLite storage for persisted settings (null for in-memory mode) */
|
|
409
|
+
private storage: AgentStorage | null;
|
|
381
410
|
private cwd: string | null;
|
|
382
411
|
private globalSettings: Settings;
|
|
383
412
|
private overrides: Settings;
|
|
384
413
|
private settings!: Settings;
|
|
385
414
|
private persist: boolean;
|
|
386
415
|
|
|
387
|
-
|
|
388
|
-
|
|
416
|
+
/**
|
|
417
|
+
* Private constructor - use static factory methods instead.
|
|
418
|
+
* @param storage - SQLite storage instance for persistence, or null for in-memory mode
|
|
419
|
+
* @param cwd - Current working directory for project settings discovery
|
|
420
|
+
* @param initialSettings - Initial global settings to use
|
|
421
|
+
* @param persist - Whether to persist settings changes to storage
|
|
422
|
+
*/
|
|
423
|
+
private constructor(storage: AgentStorage | null, cwd: string | null, initialSettings: Settings, persist: boolean) {
|
|
424
|
+
this.storage = storage;
|
|
389
425
|
this.cwd = cwd;
|
|
390
426
|
this.persist = persist;
|
|
391
427
|
this.globalSettings = initialSettings;
|
|
@@ -416,9 +452,15 @@ export class SettingsManager {
|
|
|
416
452
|
}
|
|
417
453
|
}
|
|
418
454
|
|
|
419
|
-
/**
|
|
455
|
+
/**
|
|
456
|
+
* Create a SettingsManager that loads from persistent SQLite storage.
|
|
457
|
+
* @param cwd - Current working directory for project settings discovery
|
|
458
|
+
* @param agentDir - Agent directory containing agent.db
|
|
459
|
+
* @returns Configured SettingsManager with merged global and user settings
|
|
460
|
+
*/
|
|
420
461
|
static create(cwd: string = process.cwd(), agentDir: string = getAgentDir()): SettingsManager {
|
|
421
|
-
const
|
|
462
|
+
const storage = AgentStorage.open(getAgentDbPath(agentDir));
|
|
463
|
+
SettingsManager.migrateLegacySettingsFile(storage, agentDir);
|
|
422
464
|
|
|
423
465
|
// Use capability API to load user-level settings from all providers
|
|
424
466
|
const result = loadSync(settingsCapability.id, { cwd });
|
|
@@ -431,29 +473,58 @@ export class SettingsManager {
|
|
|
431
473
|
}
|
|
432
474
|
}
|
|
433
475
|
|
|
434
|
-
//
|
|
435
|
-
const
|
|
436
|
-
globalSettings = deepMergeSettings(globalSettings,
|
|
476
|
+
// Load persisted settings from agent.db (legacy settings.json is migrated separately)
|
|
477
|
+
const storedSettings = SettingsManager.loadFromStorage(storage);
|
|
478
|
+
globalSettings = deepMergeSettings(globalSettings, storedSettings);
|
|
437
479
|
|
|
438
|
-
return new SettingsManager(
|
|
480
|
+
return new SettingsManager(storage, cwd, globalSettings, true);
|
|
439
481
|
}
|
|
440
482
|
|
|
441
|
-
/**
|
|
483
|
+
/**
|
|
484
|
+
* Create an in-memory SettingsManager without persistence.
|
|
485
|
+
* @param settings - Initial settings to use
|
|
486
|
+
* @returns SettingsManager that won't persist changes to disk
|
|
487
|
+
*/
|
|
442
488
|
static inMemory(settings: Partial<Settings> = {}): SettingsManager {
|
|
443
489
|
return new SettingsManager(null, null, settings, false);
|
|
444
490
|
}
|
|
445
491
|
|
|
446
|
-
|
|
447
|
-
|
|
492
|
+
/**
|
|
493
|
+
* Load settings from SQLite storage, applying any schema migrations.
|
|
494
|
+
* @param storage - AgentStorage instance, or null for in-memory mode
|
|
495
|
+
* @returns Parsed and migrated settings, or empty object if storage is null/empty
|
|
496
|
+
*/
|
|
497
|
+
private static loadFromStorage(storage: AgentStorage | null): Settings {
|
|
498
|
+
if (!storage) {
|
|
448
499
|
return {};
|
|
449
500
|
}
|
|
501
|
+
const settings = storage.getSettings();
|
|
502
|
+
if (!settings) {
|
|
503
|
+
return {};
|
|
504
|
+
}
|
|
505
|
+
return SettingsManager.migrateSettings(settings as Record<string, unknown>);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private static migrateLegacySettingsFile(storage: AgentStorage, agentDir: string): void {
|
|
509
|
+
const settingsPath = join(agentDir, "settings.json");
|
|
510
|
+
if (!existsSync(settingsPath)) return;
|
|
511
|
+
if (storage.getSettings() !== null) return;
|
|
512
|
+
|
|
450
513
|
try {
|
|
451
|
-
const content = readFileSync(
|
|
452
|
-
const
|
|
453
|
-
|
|
514
|
+
const content = readFileSync(settingsPath, "utf-8");
|
|
515
|
+
const parsed = JSON.parse(content);
|
|
516
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const migrated = SettingsManager.migrateSettings(parsed as Record<string, unknown>);
|
|
520
|
+
storage.saveSettings(migrated);
|
|
521
|
+
try {
|
|
522
|
+
renameSync(settingsPath, `${settingsPath}.bak`);
|
|
523
|
+
} catch (error) {
|
|
524
|
+
logger.warn("SettingsManager failed to backup settings.json", { error: String(error) });
|
|
525
|
+
}
|
|
454
526
|
} catch (error) {
|
|
455
|
-
|
|
456
|
-
return {};
|
|
527
|
+
logger.warn("SettingsManager failed to migrate settings.json", { error: String(error) });
|
|
457
528
|
}
|
|
458
529
|
}
|
|
459
530
|
|
|
@@ -497,24 +568,19 @@ export class SettingsManager {
|
|
|
497
568
|
this.rebuildSettings();
|
|
498
569
|
}
|
|
499
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Persist current global settings to SQLite storage and rebuild merged settings.
|
|
573
|
+
* Merges with any concurrent changes in storage before saving.
|
|
574
|
+
*/
|
|
500
575
|
private save(): void {
|
|
501
|
-
if (this.persist && this.
|
|
576
|
+
if (this.persist && this.storage) {
|
|
502
577
|
try {
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
mkdirSync(dir, { recursive: true });
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Re-read current file to preserve any settings added externally while running
|
|
509
|
-
const currentFileSettings = SettingsManager.loadFromFile(this.settingsPath);
|
|
510
|
-
// Merge: file settings as base, globalSettings (in-memory changes) as overrides
|
|
511
|
-
const mergedSettings = deepMergeSettings(currentFileSettings, this.globalSettings);
|
|
578
|
+
const currentSettings = this.storage.getSettings() ?? {};
|
|
579
|
+
const mergedSettings = deepMergeSettings(currentSettings, this.globalSettings);
|
|
512
580
|
this.globalSettings = mergedSettings;
|
|
513
|
-
|
|
514
|
-
// Save merged settings (project settings are read-only)
|
|
515
|
-
writeFileSync(this.settingsPath, JSON.stringify(this.globalSettings, null, 2), "utf-8");
|
|
581
|
+
this.storage.saveSettings(this.globalSettings);
|
|
516
582
|
} catch (error) {
|
|
517
|
-
|
|
583
|
+
logger.warn("SettingsManager save failed", { error: String(error) });
|
|
518
584
|
}
|
|
519
585
|
}
|
|
520
586
|
|
|
@@ -2,6 +2,7 @@ import { slashCommandCapability } from "../capability/slash-command";
|
|
|
2
2
|
import type { SlashCommand } from "../discovery";
|
|
3
3
|
import { loadSync } from "../discovery";
|
|
4
4
|
import { parseFrontmatter } from "../discovery/helpers";
|
|
5
|
+
import { renderPromptTemplate } from "./prompt-templates";
|
|
5
6
|
import { EMBEDDED_COMMAND_TEMPLATES } from "./tools/task/commands";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -158,7 +159,9 @@ export function expandSlashCommand(text: string, fileCommands: FileSlashCommand[
|
|
|
158
159
|
const fileCommand = fileCommands.find((cmd) => cmd.name === commandName);
|
|
159
160
|
if (fileCommand) {
|
|
160
161
|
const args = parseCommandArgs(argsString);
|
|
161
|
-
|
|
162
|
+
const argsText = args.join(" ");
|
|
163
|
+
const substituted = substituteArgs(fileCommand.content, args);
|
|
164
|
+
return renderPromptTemplate(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
|
|
162
165
|
}
|
|
163
166
|
|
|
164
167
|
return text;
|