@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.
- package/CHANGELOG.md +66 -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-session.ts +3 -3
- 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/export-html/index.ts +1 -33
- 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 +130 -290
- 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/complete.ts +5 -2
- 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 +5 -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/discovery/builtin.ts +9 -54
- package/src/discovery/claude.ts +16 -69
- package/src/discovery/codex.ts +11 -36
- package/src/discovery/helpers.ts +52 -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 +363 -3139
- 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/agents/planner.md +112 -0
- package/src/prompts/agents/task.md +15 -0
- 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 +237 -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 +34 -22
- package/src/core/tools/rulebook.ts +0 -132
- package/src/prompts/architect-plan.md +0 -10
- package/src/prompts/implement-with-critic.md +0 -11
- package/src/prompts/implement.md +0 -11
- package/src/prompts/system-prompt.md +0 -43
- package/src/prompts/task.md +0 -14
- package/src/prompts/title-system.md +0 -8
- /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
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
|
-
import type { AgentState
|
|
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
|
-
|
|
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,
|