march-cli 0.1.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/bin/march.mjs +13 -0
- package/package.json +36 -0
- package/src/agent/command-exec-tool.mjs +91 -0
- package/src/agent/context-stats-tool.mjs +57 -0
- package/src/agent/editing/diff-apply.mjs +28 -0
- package/src/agent/editing/diff-format.mjs +57 -0
- package/src/agent/file-edit-tool.mjs +276 -0
- package/src/agent/find-tool.mjs +112 -0
- package/src/agent/model-payload-dumper.mjs +201 -0
- package/src/agent/pi-session/pi-session-sidecar-failure.mjs +10 -0
- package/src/agent/provider/payload-messages.mjs +138 -0
- package/src/agent/read-file-tool.mjs +112 -0
- package/src/agent/runner/fast-model.mjs +36 -0
- package/src/agent/runner/runner-cleanup.mjs +12 -0
- package/src/agent/runner/runner-init.mjs +15 -0
- package/src/agent/runner/runner-session-state.mjs +40 -0
- package/src/agent/runner.mjs +266 -0
- package/src/agent/runtime/runner-runtime-host.mjs +73 -0
- package/src/agent/runtime/runtime-factory.mjs +42 -0
- package/src/agent/runtime/runtime-host.mjs +34 -0
- package/src/agent/session/session-auto-name.mjs +41 -0
- package/src/agent/session/session-binding.mjs +12 -0
- package/src/agent/session/session-options.mjs +46 -0
- package/src/agent/tool-names.mjs +1 -0
- package/src/agent/tool-result.mjs +3 -0
- package/src/agent/tools.mjs +54 -0
- package/src/agent/turn/turn-events.mjs +64 -0
- package/src/agent/turn/turn-runner.mjs +103 -0
- package/src/auth/login-command.mjs +90 -0
- package/src/auth/storage.mjs +33 -0
- package/src/cli/args.mjs +71 -0
- package/src/cli/commands/copy-command.mjs +73 -0
- package/src/cli/commands/export-command.mjs +206 -0
- package/src/cli/commands/extensions-command.mjs +53 -0
- package/src/cli/commands/help-command.mjs +7 -0
- package/src/cli/commands/model-command.mjs +110 -0
- package/src/cli/commands/paste-image-command.mjs +43 -0
- package/src/cli/commands/provider-command.mjs +55 -0
- package/src/cli/commands/status-command.mjs +157 -0
- package/src/cli/commands/thinking-command.mjs +80 -0
- package/src/cli/fallback-ui.mjs +156 -0
- package/src/cli/input/attachment-tokens.mjs +20 -0
- package/src/cli/input/autocomplete.mjs +106 -0
- package/src/cli/input/external-editor.mjs +39 -0
- package/src/cli/input/history-store.mjs +35 -0
- package/src/cli/input/image-clipboard.mjs +55 -0
- package/src/cli/input/keybinding-dispatch.mjs +76 -0
- package/src/cli/input/keybindings.mjs +96 -0
- package/src/cli/input/mode-state.mjs +43 -0
- package/src/cli/input/prompt-templates.mjs +84 -0
- package/src/cli/input/select-with-keyboard.mjs +67 -0
- package/src/cli/permissions.mjs +103 -0
- package/src/cli/repl-commands.mjs +86 -0
- package/src/cli/repl-loop.mjs +157 -0
- package/src/cli/selector-list.mjs +21 -0
- package/src/cli/session/pi-session-switch-command.mjs +41 -0
- package/src/cli/session/session-command.mjs +23 -0
- package/src/cli/session/session-list-command.mjs +68 -0
- package/src/cli/session/session-name-command.mjs +26 -0
- package/src/cli/session/session-source-command.mjs +89 -0
- package/src/cli/session/session-switch-command.mjs +1 -0
- package/src/cli/shell/shell-command.mjs +55 -0
- package/src/cli/shell/shell-drawer-controls.mjs +33 -0
- package/src/cli/shell/shell-drawer.mjs +192 -0
- package/src/cli/shell/shell-split-layout.mjs +70 -0
- package/src/cli/slash-commands.mjs +176 -0
- package/src/cli/startup/startup-banner.mjs +17 -0
- package/src/cli/startup/startup-session.mjs +51 -0
- package/src/cli/status-line-updater.mjs +74 -0
- package/src/cli/tool-output.mjs +9 -0
- package/src/cli/tui/editor/external-editor-runner.mjs +24 -0
- package/src/cli/tui/input/mouse-selection-controller.mjs +89 -0
- package/src/cli/tui/input/mouse-tracking.mjs +20 -0
- package/src/cli/tui/layout/main-pane-layout.mjs +38 -0
- package/src/cli/tui/layout/safe-render-boundary.mjs +46 -0
- package/src/cli/tui/markdown-renderer.mjs +279 -0
- package/src/cli/tui/output/scroll-state.mjs +79 -0
- package/src/cli/tui/output/tool-card-renderer.mjs +59 -0
- package/src/cli/tui/output-buffer.mjs +297 -0
- package/src/cli/tui/permission-request-ui.mjs +18 -0
- package/src/cli/tui/recall-rendering.mjs +25 -0
- package/src/cli/tui/select/editor-select-list.mjs +111 -0
- package/src/cli/tui/selection-screen.mjs +212 -0
- package/src/cli/tui/status/retry-status.mjs +72 -0
- package/src/cli/tui/status/spinner-status.mjs +42 -0
- package/src/cli/tui/status/status-bar.mjs +88 -0
- package/src/cli/tui/syntax/highlighting.mjs +277 -0
- package/src/cli/tui/syntax/languages.mjs +91 -0
- package/src/cli/tui/syntax/tree-sitter/bash.highlights.scm +261 -0
- package/src/cli/tui/syntax/tree-sitter/c.highlights.scm +341 -0
- package/src/cli/tui/syntax/tree-sitter/cpp.highlights.scm +268 -0
- package/src/cli/tui/syntax/tree-sitter/csharp.highlights.scm +577 -0
- package/src/cli/tui/syntax/tree-sitter/css.highlights.scm +109 -0
- package/src/cli/tui/syntax/tree-sitter/diff.highlights.scm +49 -0
- package/src/cli/tui/syntax/tree-sitter/go.highlights.scm +254 -0
- package/src/cli/tui/syntax/tree-sitter/html.highlights.scm +13 -0
- package/src/cli/tui/syntax/tree-sitter/java.highlights.scm +330 -0
- package/src/cli/tui/syntax/tree-sitter/json.highlights.scm +38 -0
- package/src/cli/tui/syntax/tree-sitter/php.highlights.scm +203 -0
- package/src/cli/tui/syntax/tree-sitter/python.highlights.scm +137 -0
- package/src/cli/tui/syntax/tree-sitter/ruby.highlights.scm +309 -0
- package/src/cli/tui/syntax/tree-sitter/rust.highlights.scm +531 -0
- package/src/cli/tui/syntax/tree-sitter/toml.highlights.scm +39 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-bash.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-c-sharp.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-c.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-cpp.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-css.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-diff.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-go.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-html.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-java.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-json.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-php.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-python.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-ruby.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-rust.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-toml.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-tsx.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-typescript.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tree-sitter-yaml.wasm +0 -0
- package/src/cli/tui/syntax/tree-sitter/tsx.highlights.scm +35 -0
- package/src/cli/tui/syntax/tree-sitter/typescript.highlights.scm +35 -0
- package/src/cli/tui/syntax/tree-sitter/yaml.highlights.scm +99 -0
- package/src/cli/tui/tool-rendering.mjs +194 -0
- package/src/cli/tui/tui-diff-rendering.mjs +157 -0
- package/src/cli/tui/tui-handlers.mjs +110 -0
- package/src/cli/tui/tui-input-controller.mjs +61 -0
- package/src/cli/tui/ui-theme.mjs +148 -0
- package/src/cli/ui.mjs +299 -0
- package/src/config/config-json.mjs +73 -0
- package/src/config/dotenv.mjs +20 -0
- package/src/config/features.mjs +75 -0
- package/src/config/loader.mjs +109 -0
- package/src/config/settings-command.mjs +97 -0
- package/src/context/diagnostics.mjs +70 -0
- package/src/context/engine.mjs +148 -0
- package/src/context/injections.mjs +26 -0
- package/src/context/project-context.mjs +20 -0
- package/src/context/session-status.mjs +15 -0
- package/src/context/shell-layers.mjs +23 -0
- package/src/context/system-core/base.md +60 -0
- package/src/context/system-core/prompts/deepseek-v4-pro.md +3 -0
- package/src/context/system-core/prompts/default.md +3 -0
- package/src/context/system-core.mjs +35 -0
- package/src/debug/model-context-dumper.mjs +52 -0
- package/src/extensions/discovery.mjs +40 -0
- package/src/extensions/lifecycle-adapter.mjs +210 -0
- package/src/extensions/lifecycle-manifest.mjs +69 -0
- package/src/image-gen/index.mjs +7 -0
- package/src/image-gen/provider.mjs +231 -0
- package/src/image-gen/tool.mjs +84 -0
- package/src/lsp/client.mjs +204 -0
- package/src/lsp/diagnostic-store.mjs +39 -0
- package/src/lsp/servers.mjs +212 -0
- package/src/lsp/service.mjs +65 -0
- package/src/main.mjs +294 -0
- package/src/mcp/client.mjs +195 -0
- package/src/mcp/config.mjs +130 -0
- package/src/mcp/index.mjs +48 -0
- package/src/mcp/tools.mjs +98 -0
- package/src/memory/database.mjs +219 -0
- package/src/memory/glossary.mjs +124 -0
- package/src/memory/graph/graph-cascades.mjs +109 -0
- package/src/memory/graph/graph-diagnostics.mjs +73 -0
- package/src/memory/graph/graph-path-removal.mjs +50 -0
- package/src/memory/graph/graph-path-utils.mjs +17 -0
- package/src/memory/graph/graph-primitives.mjs +103 -0
- package/src/memory/graph/graph-read.mjs +159 -0
- package/src/memory/graph.mjs +282 -0
- package/src/memory/markdown/markdown-delete.mjs +23 -0
- package/src/memory/markdown/markdown-format.mjs +128 -0
- package/src/memory/markdown/markdown-recall.mjs +28 -0
- package/src/memory/markdown/ripgrep.mjs +16 -0
- package/src/memory/markdown/sqlite-index.mjs +87 -0
- package/src/memory/markdown-store.mjs +286 -0
- package/src/memory/markdown-tools.mjs +103 -0
- package/src/memory/search.mjs +142 -0
- package/src/memory/snapshot.mjs +86 -0
- package/src/memory/system-views.mjs +120 -0
- package/src/memory/tools.mjs +282 -0
- package/src/notification/desktop-notifier.mjs +85 -0
- package/src/platform/open-file.mjs +28 -0
- package/src/provider/config-command.mjs +129 -0
- package/src/provider/presets.mjs +72 -0
- package/src/session/attachment-display.mjs +16 -0
- package/src/session/attachment-references.mjs +65 -0
- package/src/session/attachments.mjs +140 -0
- package/src/session/persist.mjs +1 -0
- package/src/session/pi-manager.mjs +34 -0
- package/src/session/session-utils.mjs +16 -0
- package/src/session/sidecar-sync.mjs +19 -0
- package/src/session/sidecar.mjs +68 -0
- package/src/session/transcript.mjs +83 -0
- package/src/session/tree.mjs +42 -0
- package/src/shell/cli-runtime.mjs +11 -0
- package/src/shell/hints.mjs +12 -0
- package/src/shell/node-pty-adapter.mjs +81 -0
- package/src/shell/runtime-state.mjs +126 -0
- package/src/shell/runtime.mjs +244 -0
- package/src/shell/screen-buffer.mjs +136 -0
- package/src/shell/tool-read.mjs +74 -0
- package/src/shell/tools.mjs +299 -0
- package/src/supergrok/actions/image-generate.mjs +60 -0
- package/src/supergrok/actions/search.mjs +78 -0
- package/src/supergrok/auth.mjs +36 -0
- package/src/supergrok/constants.mjs +18 -0
- package/src/supergrok/oauth-provider.mjs +278 -0
- package/src/supergrok/provider.mjs +36 -0
- package/src/supergrok/response.mjs +76 -0
- package/src/supergrok/tool.mjs +61 -0
- package/src/text/ansi.mjs +3 -0
- package/src/web/config-command.mjs +43 -0
- package/src/web/fetch.mjs +78 -0
- package/src/web/presets.mjs +16 -0
- package/src/web/search.mjs +83 -0
- package/src/web/tools.mjs +107 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Priority (last wins):
|
|
7
|
+
* 1. ~/.march/config — legacy global defaults
|
|
8
|
+
* 2. ~/.march/config.json — global config
|
|
9
|
+
* 3. <cwd>/.marchrc — legacy project overrides
|
|
10
|
+
* 4. <cwd>/.march/config — legacy project dir overrides
|
|
11
|
+
* 5. <cwd>/.march/config.json — project config
|
|
12
|
+
* Scalar values override.
|
|
13
|
+
*/
|
|
14
|
+
export function loadConfig(cwd, { homeDir = homedir() } = {}) {
|
|
15
|
+
const layers = [];
|
|
16
|
+
|
|
17
|
+
// 1. Global config: ~/.march/config
|
|
18
|
+
const globalPath = join(homeDir, ".march", "config");
|
|
19
|
+
layers.push(loadJson(globalPath));
|
|
20
|
+
|
|
21
|
+
layers.push(loadJson(join(homeDir, ".march", "config.json")));
|
|
22
|
+
|
|
23
|
+
// 2. Project config: <cwd>/.marchrc
|
|
24
|
+
layers.push(loadJson(join(cwd, ".marchrc")));
|
|
25
|
+
|
|
26
|
+
// 3. Project dir config: <cwd>/.march/config
|
|
27
|
+
layers.push(loadJson(join(cwd, ".march", "config")));
|
|
28
|
+
layers.push(loadJson(join(cwd, ".march", "config.json")));
|
|
29
|
+
|
|
30
|
+
return mergeLayers(layers);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadJson(path) {
|
|
34
|
+
if (!existsSync(path)) return null;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function mergeLayers(layers) {
|
|
43
|
+
const result = {
|
|
44
|
+
model: null,
|
|
45
|
+
provider: null,
|
|
46
|
+
serviceTier: null,
|
|
47
|
+
providers: {},
|
|
48
|
+
webSearch: { provider: null, providers: {} },
|
|
49
|
+
maxTurns: null,
|
|
50
|
+
trimBatch: null,
|
|
51
|
+
memoryRoot: null,
|
|
52
|
+
notifications: { turnEnd: false },
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
for (const layer of layers) {
|
|
56
|
+
if (!layer) continue;
|
|
57
|
+
if (layer.model != null) result.model = layer.model;
|
|
58
|
+
if (layer.provider) result.provider = layer.provider;
|
|
59
|
+
if (layer.serviceTier) result.serviceTier = layer.serviceTier;
|
|
60
|
+
if (layer.providers && typeof layer.providers === "object" && !Array.isArray(layer.providers)) {
|
|
61
|
+
result.providers = mergeProviders(result.providers, layer.providers);
|
|
62
|
+
}
|
|
63
|
+
if (layer.webSearch && typeof layer.webSearch === "object" && !Array.isArray(layer.webSearch)) {
|
|
64
|
+
result.webSearch = mergeWebSearch(result.webSearch, layer.webSearch);
|
|
65
|
+
}
|
|
66
|
+
if (layer.notifications && typeof layer.notifications === "object" && !Array.isArray(layer.notifications)) {
|
|
67
|
+
result.notifications = {
|
|
68
|
+
...result.notifications,
|
|
69
|
+
...layer.notifications,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (layer.memoryRoot) result.memoryRoot = layer.memoryRoot;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function mergeWebSearch(current, next) {
|
|
79
|
+
const merged = {
|
|
80
|
+
provider: next.provider ?? current.provider ?? null,
|
|
81
|
+
providers: { ...(current.providers ?? {}) },
|
|
82
|
+
};
|
|
83
|
+
if (next.providers && typeof next.providers === "object" && !Array.isArray(next.providers)) {
|
|
84
|
+
for (const [id, profile] of Object.entries(next.providers)) {
|
|
85
|
+
if (!profile || typeof profile !== "object" || Array.isArray(profile)) continue;
|
|
86
|
+
merged.providers[id] = {
|
|
87
|
+
...(merged.providers[id] ?? {}),
|
|
88
|
+
...profile,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return merged;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function mergeProviders(current, next) {
|
|
96
|
+
const merged = { ...current };
|
|
97
|
+
for (const [id, profile] of Object.entries(next)) {
|
|
98
|
+
if (!profile || typeof profile !== "object" || Array.isArray(profile)) continue;
|
|
99
|
+
merged[id] = {
|
|
100
|
+
...(merged[id] ?? {}),
|
|
101
|
+
...profile,
|
|
102
|
+
auth: {
|
|
103
|
+
...(merged[id]?.auth ?? {}),
|
|
104
|
+
...(profile.auth ?? {}),
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return merged;
|
|
109
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { loadConfig } from "./loader.mjs";
|
|
5
|
+
|
|
6
|
+
const WRITABLE_KEYS = new Set(["memoryRoot"]);
|
|
7
|
+
const SCOPES = new Set(["global", "project"]);
|
|
8
|
+
|
|
9
|
+
export function parseSettingsCommand(input) {
|
|
10
|
+
if (input !== "/settings" && !input.startsWith("/settings ")) return { type: "none" };
|
|
11
|
+
const arg = input.slice("/settings".length).trim();
|
|
12
|
+
if (!arg) return { type: "view" };
|
|
13
|
+
|
|
14
|
+
const setMatch = arg.match(/^set\s+(global|project)\s+(memoryRoot)\s+(.+)$/);
|
|
15
|
+
if (setMatch) {
|
|
16
|
+
return { type: "set", scope: setMatch[1], key: setMatch[2], value: setMatch[3].trim() };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const unsetMatch = arg.match(/^unset\s+(global|project)\s+(memoryRoot)$/);
|
|
20
|
+
if (unsetMatch) {
|
|
21
|
+
return { type: "unset", scope: unsetMatch[1], key: unsetMatch[2] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return { type: "error", message: "Usage: /settings [set <global|project> memoryRoot <value> | unset <global|project> memoryRoot]" };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function handleSettingsCommand(command, { cwd = process.cwd(), homeDir = homedir() } = {}) {
|
|
28
|
+
if (command.type === "view") return formatSettingsView({ cwd, homeDir });
|
|
29
|
+
if (command.type === "error") return [`Error: ${command.message}`];
|
|
30
|
+
if (!SCOPES.has(command.scope) || !WRITABLE_KEYS.has(command.key)) {
|
|
31
|
+
return ["Error: unsupported settings scope or key"];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const path = settingsPathForScope(command.scope, { cwd, homeDir });
|
|
35
|
+
const data = readSettingsFile(path);
|
|
36
|
+
if (command.type === "set") {
|
|
37
|
+
data[command.key] = command.value;
|
|
38
|
+
} else if (command.type === "unset") {
|
|
39
|
+
delete data[command.key];
|
|
40
|
+
}
|
|
41
|
+
writeSettingsFile(path, data);
|
|
42
|
+
return [
|
|
43
|
+
`Settings ${command.type === "set" ? "updated" : "unset"}: ${command.scope}.${command.key}`,
|
|
44
|
+
"Changes apply on next March startup; current session runtime is unchanged.",
|
|
45
|
+
...formatSettingsView({ cwd, homeDir }),
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatSettingsView({ cwd = process.cwd(), homeDir = homedir() } = {}) {
|
|
50
|
+
const globalPath = settingsPathForScope("global", { cwd, homeDir });
|
|
51
|
+
const projectPath = settingsPathForScope("project", { cwd, homeDir });
|
|
52
|
+
const globalSettings = readSettingsFile(globalPath);
|
|
53
|
+
const projectSettings = readSettingsFile(projectPath);
|
|
54
|
+
const merged = loadConfig(cwd, { homeDir });
|
|
55
|
+
return [
|
|
56
|
+
"Settings:",
|
|
57
|
+
` configured.providers: ${Object.keys(merged.providers ?? {}).join(", ") || "(none)"}`,
|
|
58
|
+
` global: ${globalPath}`,
|
|
59
|
+
...formatScopeLines(globalSettings),
|
|
60
|
+
` project: ${projectPath}`,
|
|
61
|
+
...formatScopeLines(projectSettings),
|
|
62
|
+
"Commands:",
|
|
63
|
+
" /settings set project memoryRoot <path>",
|
|
64
|
+
" /settings unset project memoryRoot",
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function settingsPathForScope(scope, { cwd, homeDir }) {
|
|
69
|
+
return scope === "global"
|
|
70
|
+
? join(homeDir, ".march", "config")
|
|
71
|
+
: join(cwd, ".march", "config");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readSettingsFile(path) {
|
|
75
|
+
if (!existsSync(path)) return {};
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
78
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
79
|
+
} catch {
|
|
80
|
+
return {};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function writeSettingsFile(path, data) {
|
|
85
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
86
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf8");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatScopeLines(settings) {
|
|
90
|
+
const keys = Object.keys(settings).sort();
|
|
91
|
+
if (keys.length === 0) return [" (empty)"];
|
|
92
|
+
return keys.map((key) => ` ${key}: ${formatSettingValue(settings[key])}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatSettingValue(value) {
|
|
96
|
+
return Array.isArray(value) ? value.join(", ") : String(value);
|
|
97
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const MAX_DIAGNOSTICS = 20;
|
|
2
|
+
|
|
3
|
+
export function buildDiagnosticsLayer({ snapshot } = {}) {
|
|
4
|
+
const diagnostics = snapshot?.diagnostics ?? [];
|
|
5
|
+
if (diagnostics.length === 0) return "[diagnostics]";
|
|
6
|
+
|
|
7
|
+
const sorted = [...diagnostics].sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
|
|
8
|
+
|
|
9
|
+
const counts = countSeverities(diagnostics);
|
|
10
|
+
const lines = ["[diagnostics]", "source: lsp"];
|
|
11
|
+
if (snapshot?.status) lines.push(`status: ${snapshot.status}`);
|
|
12
|
+
lines.push(`summary: ${formatSummary(counts)}${diagnostics.length > MAX_DIAGNOSTICS ? `, showing ${MAX_DIAGNOSTICS} of ${diagnostics.length}` : ""}`);
|
|
13
|
+
lines.push("");
|
|
14
|
+
for (const diagnostic of sorted.slice(0, MAX_DIAGNOSTICS)) {
|
|
15
|
+
lines.push(formatDiagnostic(diagnostic));
|
|
16
|
+
}
|
|
17
|
+
return lines.join("\n");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildDiagnosticsForPath({ snapshot, path } = {}) {
|
|
21
|
+
const targetPath = String(path ?? "");
|
|
22
|
+
if (!targetPath) return "";
|
|
23
|
+
const diagnostics = (snapshot?.diagnostics ?? []).filter((diagnostic) => diagnostic.path === targetPath);
|
|
24
|
+
if (diagnostics.length === 0) return "";
|
|
25
|
+
return buildDiagnosticsLayer({
|
|
26
|
+
snapshot: {
|
|
27
|
+
...snapshot,
|
|
28
|
+
diagnostics,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatDiagnostic(diagnostic) {
|
|
34
|
+
const severity = formatSeverity(diagnostic.severity);
|
|
35
|
+
const line = (diagnostic.range?.start?.line ?? 0) + 1;
|
|
36
|
+
const col = (diagnostic.range?.start?.character ?? 0) + 1;
|
|
37
|
+
const code = diagnostic.code == null ? "" : ` ${diagnostic.code}`;
|
|
38
|
+
const source = diagnostic.source ?? diagnostic.serverId ?? "lsp";
|
|
39
|
+
return `- ${severity} ${source} ${diagnostic.path}:${line}:${col}${code}\n ${singleLine(diagnostic.message ?? "")}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function countSeverities(diagnostics) {
|
|
43
|
+
const counts = { error: 0, warning: 0, info: 0, hint: 0 };
|
|
44
|
+
for (const diagnostic of diagnostics) counts[formatSeverity(diagnostic.severity)]++;
|
|
45
|
+
return counts;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function formatSummary(counts) {
|
|
49
|
+
return [
|
|
50
|
+
[counts.error, "errors"],
|
|
51
|
+
[counts.warning, "warnings"],
|
|
52
|
+
[counts.info, "info"],
|
|
53
|
+
[counts.hint, "hints"],
|
|
54
|
+
].filter(([count]) => count > 0).map(([count, label]) => `${count} ${label}`).join(", ") || "0 diagnostics";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function formatSeverity(severity) {
|
|
58
|
+
if (severity === 2) return "warning";
|
|
59
|
+
if (severity === 3) return "info";
|
|
60
|
+
if (severity === 4) return "hint";
|
|
61
|
+
return "error";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function severityRank(severity) {
|
|
65
|
+
return { error: 0, warning: 1, info: 2, hint: 3 }[formatSeverity(severity)] ?? 4;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function singleLine(text) {
|
|
69
|
+
return String(text).replace(/\s+/g, " ").trim();
|
|
70
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { buildSessionIdentity } from "./session-status.mjs";
|
|
3
|
+
import { buildSystemCore, resolveSystemCorePromptKey } from "./system-core.mjs";
|
|
4
|
+
import { buildInjectionsLayer } from "./injections.mjs";
|
|
5
|
+
import { buildProjectContext } from "./project-context.mjs";
|
|
6
|
+
import { formatRecallHints } from "../memory/markdown-store.mjs";
|
|
7
|
+
|
|
8
|
+
export class ContextEngine {
|
|
9
|
+
constructor({ cwd, modelId, provider = "deepseek", thinkingLevel = "medium", namespace = "", shellRuntime = null, lspService = null, injections = [], maxTurns, trimBatch }) {
|
|
10
|
+
this.cwd = cwd;
|
|
11
|
+
this.modelId = modelId;
|
|
12
|
+
this.provider = provider;
|
|
13
|
+
this.thinkingLevel = thinkingLevel;
|
|
14
|
+
this.turns = [];
|
|
15
|
+
this.sessionName = "";
|
|
16
|
+
this.toolDefs = [];
|
|
17
|
+
this.namespace = namespace;
|
|
18
|
+
this.shellRuntime = shellRuntime;
|
|
19
|
+
this.lspService = lspService;
|
|
20
|
+
this.injections = [...injections];
|
|
21
|
+
this.maxTurns = maxTurns ?? 15;
|
|
22
|
+
this.trimBatch = trimBatch ?? 5;
|
|
23
|
+
this.systemCorePromptKey = resolveSystemCorePromptKey({ modelId });
|
|
24
|
+
this.systemCore = buildSystemCore({ modelId });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Public API ──────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
buildContext(userMessage = "") {
|
|
30
|
+
return this.buildContextLayers(userMessage).map((layer) => layer.text).join("\n\n");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
buildProviderContext(userMessage = "") {
|
|
34
|
+
const layers = this.buildContextLayers(userMessage);
|
|
35
|
+
const systemLayer = layers.find((layer) => layer.name === "system_core");
|
|
36
|
+
return {
|
|
37
|
+
system: systemLayer?.text ?? this.systemCore,
|
|
38
|
+
userMessages: layers
|
|
39
|
+
.filter((layer) => layer.name !== "system_core")
|
|
40
|
+
.map((layer) => ({
|
|
41
|
+
name: layer.name,
|
|
42
|
+
content: layer.name === "recent_chat" ? appendCurrentUser(layer.text, userMessage) : layer.text,
|
|
43
|
+
})),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
buildContextLayers(_userMessage = "") {
|
|
48
|
+
const layers = [
|
|
49
|
+
{ name: "system_core", text: this.systemCore },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const injectionsLayer = buildInjectionsLayer(this.injections);
|
|
53
|
+
if (injectionsLayer) layers.push({ name: "injections", text: injectionsLayer });
|
|
54
|
+
|
|
55
|
+
layers.push({ name: "session_identity", text: this.#buildSessionIdentity() });
|
|
56
|
+
|
|
57
|
+
const projectCtx = buildProjectContext(this.cwd);
|
|
58
|
+
if (projectCtx) layers.push({ name: "project_context", text: projectCtx });
|
|
59
|
+
|
|
60
|
+
layers.push({ name: "recent_chat", text: this.#buildRecentChat() });
|
|
61
|
+
|
|
62
|
+
return layers;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
recordTurn({ userMessage, assistantMessage, userRecallHints = [], assistantRecallHints = [] }) {
|
|
66
|
+
this.turns.push({
|
|
67
|
+
index: this.turns.length + 1,
|
|
68
|
+
userMessage,
|
|
69
|
+
assistantMessage: assistantMessage ?? "",
|
|
70
|
+
userRecallHints,
|
|
71
|
+
assistantRecallHints,
|
|
72
|
+
});
|
|
73
|
+
if (this.turns.length > this.maxTurns) {
|
|
74
|
+
const keep = Math.max(1, this.maxTurns - this.trimBatch);
|
|
75
|
+
this.turns = this.turns.slice(-keep);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
getRecentRecallMemoryIds() {
|
|
80
|
+
const ids = new Set();
|
|
81
|
+
for (const turn of this.turns) {
|
|
82
|
+
for (const hint of turn.userRecallHints ?? []) if (hint?.id) ids.add(hint.id);
|
|
83
|
+
for (const hint of turn.assistantRecallHints ?? []) if (hint?.id) ids.add(hint.id);
|
|
84
|
+
}
|
|
85
|
+
return ids;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
resolvePath(raw) {
|
|
89
|
+
return resolve(this.cwd, raw);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setInjections(injections = []) { this.injections = [...injections]; }
|
|
93
|
+
setToolDefs(defs) { this.toolDefs = defs; }
|
|
94
|
+
setRuntimeState({ modelId, provider, thinkingLevel } = {}) {
|
|
95
|
+
if (modelId) this.modelId = modelId;
|
|
96
|
+
if (provider) this.provider = provider;
|
|
97
|
+
if (thinkingLevel) this.thinkingLevel = thinkingLevel;
|
|
98
|
+
const nextPromptKey = resolveSystemCorePromptKey({ modelId: this.modelId });
|
|
99
|
+
if (nextPromptKey !== this.systemCorePromptKey) {
|
|
100
|
+
this.systemCorePromptKey = nextPromptKey;
|
|
101
|
+
this.systemCore = buildSystemCore({ modelId: this.modelId });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
setSessionName(name) { this.sessionName = String(name || "").trim(); }
|
|
105
|
+
|
|
106
|
+
restoreSession(data, _pool, { replace = false } = {}) {
|
|
107
|
+
if (replace) {
|
|
108
|
+
this.turns = [];
|
|
109
|
+
this.sessionName = "";
|
|
110
|
+
}
|
|
111
|
+
if (data.turns) this.turns = data.turns;
|
|
112
|
+
if (typeof data.sessionName === "string") this.sessionName = data.sessionName;
|
|
113
|
+
this.setRuntimeState(data);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Private layers ──────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
#buildSessionIdentity() {
|
|
119
|
+
return buildSessionIdentity({ cwd: this.cwd, workspaceRoot: this.cwd });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#buildRecentChat() {
|
|
123
|
+
if (this.turns.length === 0) {
|
|
124
|
+
return `[recent_chat]\n(no prior turns)`;
|
|
125
|
+
}
|
|
126
|
+
const entries = [];
|
|
127
|
+
for (const turn of this.turns) {
|
|
128
|
+
let block = `## Turn ${turn.index}\n` +
|
|
129
|
+
`[user]\n${String(turn.userMessage ?? "")}\n`;
|
|
130
|
+
const userRecall = formatRecallHints("user", turn.userRecallHints ?? []);
|
|
131
|
+
if (userRecall) block += `\n${userRecall}\n`;
|
|
132
|
+
block += `\n[March]\n`;
|
|
133
|
+
if (turn.assistantMessage) {
|
|
134
|
+
block += `\n${String(turn.assistantMessage ?? "")}\n`;
|
|
135
|
+
}
|
|
136
|
+
const assistantRecall = formatRecallHints("assistant", turn.assistantRecallHints ?? []);
|
|
137
|
+
if (assistantRecall) block += `\n${assistantRecall}\n`;
|
|
138
|
+
entries.push(block);
|
|
139
|
+
}
|
|
140
|
+
return `[recent_chat]\n${entries.join("\n\n")}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function appendCurrentUser(recentChat, userMessage) {
|
|
146
|
+
const currentUser = String(userMessage ?? "").trimEnd();
|
|
147
|
+
return `${recentChat}\n\n[current_user]\n${currentUser}`;
|
|
148
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function buildInjectionsLayer(injections = []) {
|
|
2
|
+
const blocks = injections
|
|
3
|
+
.map(normalizeInjection)
|
|
4
|
+
.filter(Boolean)
|
|
5
|
+
.map(({ type, source, content }) => `## ${formatInjectionTitle(type, source)}\n${content}`);
|
|
6
|
+
|
|
7
|
+
if (blocks.length === 0) return "";
|
|
8
|
+
return `[injections]\n${blocks.join("\n\n")}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeInjection(injection) {
|
|
12
|
+
if (!injection || typeof injection.content !== "string") return null;
|
|
13
|
+
const content = injection.content.trim();
|
|
14
|
+
if (!content) return null;
|
|
15
|
+
return {
|
|
16
|
+
type: String(injection.type || "external"),
|
|
17
|
+
source: String(injection.source || "unknown"),
|
|
18
|
+
content,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatInjectionTitle(type, source) {
|
|
23
|
+
if (type === "mcp_server") return `MCP server: ${source}`;
|
|
24
|
+
if (type === "extension") return `Extension: ${source}`;
|
|
25
|
+
return `${type}: ${source}`;
|
|
26
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Build the [project_context] layer from AGENTS.md / CLAUDE.md files.
|
|
7
|
+
* Uses SDK's loadProjectContextFiles to scan:
|
|
8
|
+
* - ~/.march/ (global)
|
|
9
|
+
* - cwd upward to root (ancestor directories, closest first)
|
|
10
|
+
*
|
|
11
|
+
* Returns null if no files found.
|
|
12
|
+
*/
|
|
13
|
+
export function buildProjectContext(cwd) {
|
|
14
|
+
const agentDir = resolve(homedir(), ".march");
|
|
15
|
+
const files = loadProjectContextFiles({ cwd, agentDir });
|
|
16
|
+
if (!files || files.length === 0) return null;
|
|
17
|
+
|
|
18
|
+
const blocks = files.map((f) => `--- ${f.path} ---\n${f.content.trimEnd()}`);
|
|
19
|
+
return `[project_context]\n${blocks.join("\n\n")}`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function buildSessionIdentity({
|
|
2
|
+
cwd,
|
|
3
|
+
workspaceRoot = cwd,
|
|
4
|
+
platform = process.platform,
|
|
5
|
+
} = {}) {
|
|
6
|
+
const shellInfo = platform === "win32"
|
|
7
|
+
? "shells: powershell (recommended), bash (Git Bash)"
|
|
8
|
+
: "shell: bash";
|
|
9
|
+
|
|
10
|
+
return `[session_identity]
|
|
11
|
+
cwd: ${cwd}
|
|
12
|
+
workspace_root: ${workspaceRoot}
|
|
13
|
+
platform: ${platform}
|
|
14
|
+
${shellInfo}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function buildShellLayers({ shellRuntime, truncateText = truncate } = {}) {
|
|
2
|
+
const shells = (shellRuntime?.listShells?.() ?? []).filter((s) => s.status !== "killed");
|
|
3
|
+
if (!shells.length) return [];
|
|
4
|
+
const blocks = shells.map((shell) => {
|
|
5
|
+
const snapshot = shellRuntime.snapshotShell(shell.id);
|
|
6
|
+
const output = snapshot.plain ? truncateText(snapshot.plain, 2000) : "(no output)";
|
|
7
|
+
return [
|
|
8
|
+
`## ${shell.name} (${shell.id})`,
|
|
9
|
+
`status: ${shell.status}`,
|
|
10
|
+
`command: ${shell.command}${shell.args?.length ? ` ${shell.args.join(" ")}` : ""}`,
|
|
11
|
+
`cwd: ${shell.cwd}`,
|
|
12
|
+
`lines: ${shell.lineCount}`,
|
|
13
|
+
"recent_output:",
|
|
14
|
+
output,
|
|
15
|
+
].join("\n");
|
|
16
|
+
});
|
|
17
|
+
return [`[shells]\n${blocks.join("\n\n")}`];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function truncate(text, maxLen) {
|
|
21
|
+
if (text.length <= maxLen) return text;
|
|
22
|
+
return text.slice(0, maxLen) + "\n...(truncated)";
|
|
23
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<identity>
|
|
2
|
+
You are March, a terminal-native coding agent. You operate in the user's project directory with direct file access.
|
|
3
|
+
The user primarily asks for software engineering work: fixing bugs, adding behavior, refactoring, explaining code, and maintaining this repository. Interpret unclear requests in that project context.
|
|
4
|
+
</identity>
|
|
5
|
+
|
|
6
|
+
<communication_contract>
|
|
7
|
+
- Be concise and direct. Match the response shape to the task; simple questions get simple answers.
|
|
8
|
+
- Assume users may not see tool calls. Before the first tool call, say in one sentence what you are about to do. While working, give brief updates when you find something important, change direction, or hit a blocker.
|
|
9
|
+
- Don't narrate hidden reasoning. State decisions, results, and relevant next steps.
|
|
10
|
+
- End with one or two sentences: what changed, verification status, and what's next if anything.
|
|
11
|
+
- Report outcomes truthfully. If tests fail or a step was skipped, say so plainly with the relevant output or reason.
|
|
12
|
+
</communication_contract>
|
|
13
|
+
|
|
14
|
+
<operating_contract>
|
|
15
|
+
- Default to doing the requested work in the repository, not giving abstract advice.
|
|
16
|
+
- Build context from current project facts before editing. Inspect existing code and conventions first.
|
|
17
|
+
- Keep the change scoped to the request. Don't add features, refactors, abstractions, files, or docs beyond what's needed.
|
|
18
|
+
- Three similar lines beats a premature abstraction. No half-finished implementations.
|
|
19
|
+
- Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal guarantees; validate at real boundaries such as user input and external APIs.
|
|
20
|
+
- Avoid backwards-compatibility hacks. If unused code is truly unused, delete it rather than leaving shims or markers.
|
|
21
|
+
- Default to add one short comment when the WHY is helpful.
|
|
22
|
+
</operating_contract>
|
|
23
|
+
|
|
24
|
+
<safety_contract>
|
|
25
|
+
- Local, reversible actions such as reading files, editing files, and running tests are normally okay.
|
|
26
|
+
- Confirm before actions that are hard to reverse, destructive, outward-facing, or affect shared state: deleting user work, force operations, dependency downgrades, CI/CD changes, pushing code, creating PRs/issues, sending messages, or publishing content to external services.
|
|
27
|
+
- Before deleting or overwriting, inspect the target. If reality contradicts the request or you didn't create the state, stop and surface it.
|
|
28
|
+
- Don't bypass safeguards to make an obstacle disappear. Never skip hooks or signing unless explicitly requested; investigate failures and fix the underlying issue.
|
|
29
|
+
</safety_contract>
|
|
30
|
+
|
|
31
|
+
<editing_contract>
|
|
32
|
+
- Use read(path) for file inspection with 1-based line numbers.
|
|
33
|
+
- Use grep(pattern), find(pattern), and ls(path) to explore the project before editing.
|
|
34
|
+
- Prefer dedicated read/search/edit tools over shell commands for file inspection and modification.
|
|
35
|
+
- Use command_exec for one-shot commands. Use terminal_* only for interactive programs, long-running processes, or when preserving terminal state matters.
|
|
36
|
+
- Keep the working directory stable; use paths instead of cd unless the user asks otherwise.
|
|
37
|
+
- Use edit_file for all file writes.
|
|
38
|
+
- For targeted edits: use edit_file with mode="patch" and edits[] entries: replace_range(startLine, endLine, newText) or replace_text(oldText, newText).
|
|
39
|
+
- For new files use edit_file with mode="write" and content. For full replacement of an existing file use mode="overwrite" and content.
|
|
40
|
+
</editing_contract>
|
|
41
|
+
|
|
42
|
+
<verification_contract>
|
|
43
|
+
- Run the most relevant tests, type checks, or linters when practical after code changes.
|
|
44
|
+
- If you cannot verify, say what was not run and why.
|
|
45
|
+
- Do not claim success beyond what you actually checked.
|
|
46
|
+
</verification_contract>
|
|
47
|
+
|
|
48
|
+
<git_contract>
|
|
49
|
+
- Check worktree state before committing or making broad edits.
|
|
50
|
+
- Do not overwrite or discard user changes unless explicitly asked.
|
|
51
|
+
- When project instructions require a commit after each completed modification, create a focused commit for your change.
|
|
52
|
+
- Never use --no-verify, --no-gpg-sign, or commit.gpgsign=false unless the user explicitly asks.
|
|
53
|
+
</git_contract>
|
|
54
|
+
|
|
55
|
+
<memory_system>
|
|
56
|
+
- [memory_hint source="..."] blocks in recent_chat show memory hints matched from your thinking output. Use memory_open(id) to read the full content.
|
|
57
|
+
- Use memory_search(query) for full-text search across all memories.
|
|
58
|
+
- To edit an existing memory, use memory_open(id) to get its path, then edit_file with mode="patch" for targeted edits.
|
|
59
|
+
- Use memory_save() to create memories or update whole fields. Before creating a new memory, merge related updates into an existing memory when they share the same topic or decision thread. Tags are the primary retrieval key for future recall. Prefer lowercase kebab-case tags like 'march-cli', 'tooling', 'permissions'.
|
|
60
|
+
</memory_system>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const PROMPT_DIR = join(dirname(fileURLToPath(import.meta.url)), "system-core", "prompts");
|
|
6
|
+
const BASE_PROMPT_PATH = join(dirname(fileURLToPath(import.meta.url)), "system-core", "base.md");
|
|
7
|
+
const DEFAULT_PROMPT_KEY = "default";
|
|
8
|
+
|
|
9
|
+
export function buildSystemCore({ modelId } = {}) {
|
|
10
|
+
const prompt = loadSystemCorePrompt({ modelId });
|
|
11
|
+
return `[system_core]\n${prompt.base}\n\n${prompt.append}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveSystemCorePromptKey({ modelId = "" } = {}) {
|
|
15
|
+
const key = sanitizeModelId(modelId);
|
|
16
|
+
if (key && existsSync(promptPath(key))) return key;
|
|
17
|
+
return DEFAULT_PROMPT_KEY;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function loadSystemCorePrompt({ modelId } = {}) {
|
|
21
|
+
const key = resolveSystemCorePromptKey({ modelId });
|
|
22
|
+
return {
|
|
23
|
+
key,
|
|
24
|
+
base: readFileSync(BASE_PROMPT_PATH, "utf8").trim(),
|
|
25
|
+
append: readFileSync(promptPath(key), "utf8").trim(),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function promptPath(key) {
|
|
30
|
+
return join(PROMPT_DIR, `${key}.md`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sanitizeModelId(modelId) {
|
|
34
|
+
return String(modelId || "").trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
35
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, extname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function createModelContextDumper({ enabled = false, rootDir = "", now = () => new Date() } = {}) {
|
|
5
|
+
let sequence = 0;
|
|
6
|
+
return {
|
|
7
|
+
enabled: Boolean(enabled),
|
|
8
|
+
rootDir,
|
|
9
|
+
dump({ kind = "model", prompt = "", metadata = {} } = {}) {
|
|
10
|
+
if (!enabled) return null;
|
|
11
|
+
sequence += 1;
|
|
12
|
+
mkdirSync(rootDir, { recursive: true });
|
|
13
|
+
const timestamp = now().toISOString();
|
|
14
|
+
const filename = `${sanitizeTimestamp(timestamp)}-${String(sequence).padStart(4, "0")}-${sanitizeKind(kind)}.md`;
|
|
15
|
+
const path = join(rootDir, filename);
|
|
16
|
+
writeFileSync(path, `${formatHeader({ kind, timestamp, ...metadata })}\n\n${prompt}`, "utf8");
|
|
17
|
+
return path;
|
|
18
|
+
},
|
|
19
|
+
dumpSidecar({ sourcePath, suffix, value } = {}) {
|
|
20
|
+
if (!enabled || !sourcePath || !suffix) return null;
|
|
21
|
+
const ext = extname(sourcePath);
|
|
22
|
+
const stem = basename(sourcePath, ext);
|
|
23
|
+
const path = join(rootDir, `${stem}-${sanitizeKind(suffix)}.json`);
|
|
24
|
+
mkdirSync(rootDir, { recursive: true });
|
|
25
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
|
26
|
+
return path;
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatHeader(metadata) {
|
|
32
|
+
const lines = ["---"];
|
|
33
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
34
|
+
if (value == null || value === "") continue;
|
|
35
|
+
lines.push(`${key}: ${formatValue(value)}`);
|
|
36
|
+
}
|
|
37
|
+
lines.push("---");
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function formatValue(value) {
|
|
42
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
43
|
+
return JSON.stringify(String(value));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sanitizeTimestamp(timestamp) {
|
|
47
|
+
return timestamp.replace(/[:.]/g, "-");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sanitizeKind(kind) {
|
|
51
|
+
return String(kind).replace(/[^a-z0-9_-]+/gi, "-").replace(/^-+|-+$/g, "") || "model";
|
|
52
|
+
}
|