@oh-my-pi/pi-coding-agent 4.1.0 → 4.2.1

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 (90) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +2 -1
  3. package/docs/sdk.md +0 -3
  4. package/package.json +6 -5
  5. package/src/config.ts +9 -0
  6. package/src/core/agent-session.ts +3 -3
  7. package/src/core/agent-storage.ts +450 -0
  8. package/src/core/auth-storage.ts +102 -183
  9. package/src/core/compaction/branch-summarization.ts +5 -4
  10. package/src/core/compaction/compaction.ts +7 -6
  11. package/src/core/compaction/utils.ts +6 -11
  12. package/src/core/custom-commands/bundled/review/index.ts +22 -94
  13. package/src/core/custom-share.ts +66 -0
  14. package/src/core/export-html/index.ts +1 -33
  15. package/src/core/history-storage.ts +15 -7
  16. package/src/core/prompt-templates.ts +271 -1
  17. package/src/core/sdk.ts +14 -3
  18. package/src/core/settings-manager.ts +100 -34
  19. package/src/core/slash-commands.ts +4 -1
  20. package/src/core/storage-migration.ts +215 -0
  21. package/src/core/system-prompt.ts +130 -290
  22. package/src/core/title-generator.ts +3 -2
  23. package/src/core/tools/ask.ts +2 -2
  24. package/src/core/tools/bash.ts +2 -1
  25. package/src/core/tools/calculator.ts +2 -1
  26. package/src/core/tools/complete.ts +5 -2
  27. package/src/core/tools/edit.ts +2 -1
  28. package/src/core/tools/find.ts +2 -1
  29. package/src/core/tools/gemini-image.ts +2 -1
  30. package/src/core/tools/git.ts +2 -2
  31. package/src/core/tools/grep.ts +2 -1
  32. package/src/core/tools/index.test.ts +0 -28
  33. package/src/core/tools/index.ts +0 -6
  34. package/src/core/tools/lsp/index.ts +2 -1
  35. package/src/core/tools/output.ts +2 -1
  36. package/src/core/tools/read.ts +4 -1
  37. package/src/core/tools/ssh.ts +4 -2
  38. package/src/core/tools/task/agents.ts +56 -30
  39. package/src/core/tools/task/commands.ts +5 -8
  40. package/src/core/tools/task/index.ts +7 -15
  41. package/src/core/tools/web-fetch.ts +2 -1
  42. package/src/core/tools/web-search/auth.ts +106 -16
  43. package/src/core/tools/web-search/index.ts +3 -2
  44. package/src/core/tools/web-search/providers/anthropic.ts +44 -6
  45. package/src/core/tools/write.ts +2 -1
  46. package/src/core/voice.ts +3 -1
  47. package/src/discovery/builtin.ts +9 -54
  48. package/src/discovery/claude.ts +16 -69
  49. package/src/discovery/codex.ts +11 -36
  50. package/src/discovery/helpers.ts +52 -1
  51. package/src/main.ts +1 -1
  52. package/src/migrations.ts +20 -20
  53. package/src/modes/interactive/controllers/command-controller.ts +527 -0
  54. package/src/modes/interactive/controllers/event-controller.ts +340 -0
  55. package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
  56. package/src/modes/interactive/controllers/input-controller.ts +585 -0
  57. package/src/modes/interactive/controllers/selector-controller.ts +585 -0
  58. package/src/modes/interactive/interactive-mode.ts +363 -3139
  59. package/src/modes/interactive/theme/theme.ts +5 -5
  60. package/src/modes/interactive/types.ts +189 -0
  61. package/src/modes/interactive/utils/ui-helpers.ts +449 -0
  62. package/src/modes/interactive/utils/voice-manager.ts +96 -0
  63. package/src/prompts/{explore.md → agents/explore.md} +7 -5
  64. package/src/prompts/agents/frontmatter.md +7 -0
  65. package/src/prompts/{plan.md → agents/plan.md} +3 -3
  66. package/src/prompts/agents/planner.md +112 -0
  67. package/src/prompts/agents/task.md +15 -0
  68. package/src/prompts/review-request.md +44 -8
  69. package/src/prompts/system/custom-system-prompt.md +80 -0
  70. package/src/prompts/system/file-operations.md +12 -0
  71. package/src/prompts/system/system-prompt.md +237 -0
  72. package/src/prompts/system/title-system.md +2 -0
  73. package/src/prompts/tools/bash.md +1 -1
  74. package/src/prompts/tools/read.md +1 -1
  75. package/src/prompts/tools/task.md +34 -22
  76. package/src/core/tools/rulebook.ts +0 -132
  77. package/src/prompts/architect-plan.md +0 -10
  78. package/src/prompts/implement-with-critic.md +0 -11
  79. package/src/prompts/implement.md +0 -11
  80. package/src/prompts/system-prompt.md +0 -43
  81. package/src/prompts/task.md +0 -14
  82. package/src/prompts/title-system.md +0 -8
  83. /package/src/prompts/{init.md → agents/init.md} +0 -0
  84. /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
  85. /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
  86. /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
  87. /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
  88. /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
  89. /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
  90. /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
+ }
@@ -1,7 +1,6 @@
1
1
  import { existsSync, writeFileSync } from "node:fs";
2
2
  import { basename } from "node:path";
3
- import type { AgentState, AgentTool } from "@oh-my-pi/pi-agent-core";
4
- import { buildCodexPiBridge, getCodexInstructions } from "@oh-my-pi/pi-ai";
3
+ import type { AgentState } from "@oh-my-pi/pi-agent-core";
5
4
  import { APP_NAME } from "../../config";
6
5
  import { getResolvedThemeColors, getThemeExportColors } from "../../modes/interactive/theme/theme";
7
6
  import { SessionManager } from "../session-manager";
@@ -14,33 +13,6 @@ export interface ExportOptions {
14
13
  themeName?: string;
15
14
  }
16
15
 
17
- /** Info about Codex injection to show inline with model_change entries. */
18
- interface CodexInjectionInfo {
19
- /** Codex instructions text. */
20
- instructions: string;
21
- /** Bridge text (tool list). */
22
- bridge: string;
23
- }
24
-
25
- /** Build Codex injection info for display inline with model_change entries. */
26
- async function buildCodexInjectionInfo(tools?: AgentTool[]): Promise<CodexInjectionInfo | undefined> {
27
- let instructions: string | null = null;
28
- try {
29
- instructions = await getCodexInstructions("gpt-5.1-codex");
30
- } catch {
31
- // Cache miss is expected before the first Codex request.
32
- }
33
-
34
- const bridgeText = buildCodexPiBridge(tools);
35
- const instructionsText =
36
- instructions ?? "(Codex instructions not cached. Run a Codex request to populate the local cache.)";
37
-
38
- return {
39
- instructions: instructionsText,
40
- bridge: bridgeText,
41
- };
42
- }
43
-
44
16
  /** Parse a color string to RGB values. */
45
17
  function parseColor(color: string): { r: number; g: number; b: number } | undefined {
46
18
  const hexMatch = color.match(/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/);
@@ -125,8 +97,6 @@ interface SessionData {
125
97
  entries: ReturnType<SessionManager["getEntries"]>;
126
98
  leafId: string | null;
127
99
  systemPrompt?: string;
128
- /** Info for rendering Codex injection inline with model_change entries. */
129
- codexInjectionInfo?: CodexInjectionInfo;
130
100
  tools?: { name: string; description: string }[];
131
101
  }
132
102
 
@@ -158,7 +128,6 @@ export async function exportSessionToHtml(
158
128
  entries: sm.getEntries(),
159
129
  leafId: sm.getLeafId(),
160
130
  systemPrompt: state?.systemPrompt,
161
- codexInjectionInfo: await buildCodexInjectionInfo(state?.tools),
162
131
  tools: state?.tools?.map((t) => ({ name: t.name, description: t.description })),
163
132
  };
164
133
 
@@ -180,7 +149,6 @@ export async function exportFromFile(inputPath: string, options?: ExportOptions
180
149
  header: sm.getHeader(),
181
150
  entries: sm.getEntries(),
182
151
  leafId: sm.getLeafId(),
183
- codexInjectionInfo: await buildCodexInjectionInfo(),
184
152
  };
185
153
 
186
154
  const html = generateHtml(sessionData, opts.themeName);
@@ -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
- try {
88
- const last = this.lastPromptStmt.get() as { prompt?: string } | undefined;
89
- if (last?.prompt === trimmed) return;
94
+ this.lastPromptCache = trimmed;
90
95
 
91
- this.insertStmt.run(trimmed, cwd ?? null);
92
- } catch (error) {
93
- logger.error("HistoryStorage add failed", { error: String(error) });
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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
- return substituteArgs(template.content, args);
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 = filterRulebookRules(rulesResult.items);
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,