indusagi-coding-agent 0.2.5 → 0.2.9
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 +9 -0
- package/README.md +9 -5
- package/dist/entry.js +14267 -24282
- package/dist/guardrails.js +924 -7867
- package/dist/index.js +13706 -23775
- package/package.json +3 -2
- package/src/_decls/entry.ts +18 -0
- package/src/_decls/guardrails.ts +35 -0
- package/src/_decls/index.ts +26 -0
- package/src/addons/contract.ts +236 -0
- package/src/addons/dispatch/event-dispatcher.ts +164 -0
- package/src/addons/dispatch/index.ts +25 -0
- package/src/addons/dispatch/tool-interceptor.ts +208 -0
- package/src/addons/host.ts +225 -0
- package/src/addons/index.ts +112 -0
- package/src/addons/manifest.ts +158 -0
- package/src/addons/sandbox.ts +170 -0
- package/src/addons/surface.ts +78 -0
- package/src/boot/auth-vault.ts +195 -0
- package/src/boot/boot.ts +138 -0
- package/src/boot/contract.ts +238 -0
- package/src/boot/heap.ts +59 -0
- package/src/boot/index.ts +28 -0
- package/src/boot/invocation.ts +93 -0
- package/src/boot/runners/addon-wiring.ts +153 -0
- package/src/boot/runners/checkpoint.ts +169 -0
- package/src/boot/runners/delegate-runner.ts +294 -0
- package/src/boot/runners/index.ts +13 -0
- package/src/boot/runners/link-runner.ts +45 -0
- package/src/boot/runners/memdir.ts +168 -0
- package/src/boot/runners/oneshot-runner.ts +58 -0
- package/src/boot/runners/read-state.ts +90 -0
- package/src/boot/runners/registry.ts +42 -0
- package/src/boot/runners/repl-runner.ts +143 -0
- package/src/boot/runners/server-mode.ts +121 -0
- package/src/boot/runners/session.ts +641 -0
- package/src/boot/server-token.ts +148 -0
- package/src/boot/stages.ts +167 -0
- package/src/boot/upgrade/apply.ts +94 -0
- package/src/boot/upgrade/index.ts +13 -0
- package/src/boot/upgrade/upgrades.ts +289 -0
- package/src/briefing/compose.ts +150 -0
- package/src/briefing/context-docs.ts +19 -0
- package/src/briefing/contract.ts +717 -0
- package/src/briefing/index.ts +31 -0
- package/src/briefing/macros.ts +97 -0
- package/src/briefing/skills.ts +47 -0
- package/src/capability-deck/bridge-ledger/index.ts +27 -0
- package/src/capability-deck/bridge-ledger/key.ts +67 -0
- package/src/capability-deck/bridge-ledger/ledger.ts +131 -0
- package/src/capability-deck/bridge-ledger/network.ts +117 -0
- package/src/capability-deck/builtin-bridge.ts +312 -0
- package/src/capability-deck/cards/bg-process-card.ts +335 -0
- package/src/capability-deck/cards/index.ts +115 -0
- package/src/capability-deck/cards/memory-card.ts +146 -0
- package/src/capability-deck/cards/plan-file.ts +97 -0
- package/src/capability-deck/cards/plan-tools.ts +185 -0
- package/src/capability-deck/cards/saas-card.ts +183 -0
- package/src/capability-deck/cards/task-card.ts +207 -0
- package/src/capability-deck/cards/todo-card.ts +168 -0
- package/src/capability-deck/cards/workflow-card.ts +247 -0
- package/src/capability-deck/contract.ts +388 -0
- package/src/capability-deck/index.ts +48 -0
- package/src/capability-deck/manifest.ts +109 -0
- package/src/capability-deck/provision.ts +169 -0
- package/src/channels/contract.ts +191 -0
- package/src/channels/framer.ts +50 -0
- package/src/channels/index.ts +101 -0
- package/src/channels/link/dialog.ts +129 -0
- package/src/channels/link/driver.ts +190 -0
- package/src/channels/link/index.ts +34 -0
- package/src/channels/link/server.ts +134 -0
- package/src/channels/oneshot.ts +90 -0
- package/src/channels/ops.ts +65 -0
- package/src/channels/session-ops.ts +81 -0
- package/src/conductor/bash-guard.ts +599 -0
- package/src/conductor/catalog/catalog.ts +116 -0
- package/src/conductor/catalog/index.ts +8 -0
- package/src/conductor/catalog/matcher.ts +134 -0
- package/src/conductor/conductor.ts +234 -0
- package/src/conductor/contract.ts +842 -0
- package/src/conductor/diagnostics.ts +227 -0
- package/src/conductor/index.ts +33 -0
- package/src/conductor/permissions.ts +588 -0
- package/src/conductor/quota-error.ts +49 -0
- package/src/conductor/signal-hub/hub.ts +46 -0
- package/src/conductor/signal-hub/index.ts +2 -0
- package/src/conductor/signal-hub/translate.test.ts +81 -0
- package/src/conductor/signal-hub/translate.ts +74 -0
- package/src/conductor/skill-parse/index.ts +2 -0
- package/src/conductor/skill-parse/parse.ts +108 -0
- package/src/conductor/transcript-store/index.ts +22 -0
- package/src/conductor/transcript-store/serialize.ts +116 -0
- package/src/conductor/transcript-store/store.ts +205 -0
- package/src/console/auth-status.ts +56 -0
- package/src/console/components/AgentsView.ts +165 -0
- package/src/console/components/BackgroundAgents.ts +155 -0
- package/src/console/components/Banner.ts +334 -0
- package/src/console/components/Composer.ts +94 -0
- package/src/console/components/StatusBar.ts +49 -0
- package/src/console/components/TerminalConsole.ts +1090 -0
- package/src/console/components/WorkingIndicator.ts +98 -0
- package/src/console/components/banner-sweep.ts +24 -0
- package/src/console/components/welcome.ts +74 -0
- package/src/console/contract.ts +630 -0
- package/src/console/index.ts +34 -0
- package/src/console/input/complete.ts +127 -0
- package/src/console/input/dir-reader.ts +34 -0
- package/src/console/input/index.ts +23 -0
- package/src/console/input/keymap.ts +159 -0
- package/src/console/input/paste.ts +104 -0
- package/src/console/mount.ts +56 -0
- package/src/console/overlays/approval-queue.ts +57 -0
- package/src/console/overlays/approval.ts +130 -0
- package/src/console/overlays/auth.ts +342 -0
- package/src/console/overlays/boards.ts +308 -0
- package/src/console/overlays/host.ts +36 -0
- package/src/console/overlays/index.ts +26 -0
- package/src/console/overlays/pickers.ts +258 -0
- package/src/console/overlays/sessions.ts +190 -0
- package/src/console/reducer.ts +182 -0
- package/src/console/slash/builtins.ts +81 -0
- package/src/console/slash/commands/dynamic.ts +83 -0
- package/src/console/slash/commands/integrations.ts +695 -0
- package/src/console/slash/commands/shared.ts +75 -0
- package/src/console/slash/commands/transcript.ts +263 -0
- package/src/console/slash/commands/workbench.ts +246 -0
- package/src/console/slash/index.ts +15 -0
- package/src/console/slash/registry.ts +70 -0
- package/src/console/slash/resolve.ts +63 -0
- package/src/console/startup.ts +209 -0
- package/src/console/theme/adapter.ts +45 -0
- package/src/console/theme/index.ts +7 -0
- package/src/console/theme/palette.ts +68 -0
- package/src/console/theme/resolve.ts +39 -0
- package/src/console/theme/tokens.ts +71 -0
- package/src/entry.ts +55 -0
- package/src/guardrails.ts +37 -0
- package/src/index.ts +18 -0
- package/src/insight/channel.ts +88 -0
- package/src/insight/contract.ts +185 -0
- package/src/insight/index.ts +110 -0
- package/src/insight/recorder.ts +213 -0
- package/src/insight/redaction.ts +157 -0
- package/src/insight/replay.ts +158 -0
- package/src/insight/sampling.ts +70 -0
- package/src/insight/serialize.ts +50 -0
- package/src/insight/sinks/console.ts +64 -0
- package/src/insight/sinks/file.ts +40 -0
- package/src/insight/sinks/index.ts +24 -0
- package/src/insight/sinks/stream.ts +54 -0
- package/src/integrations/sarvam/attach.ts +239 -0
- package/src/integrations/sarvam/config.ts +156 -0
- package/src/integrations/sarvam/index.ts +25 -0
- package/src/integrations/sarvam/sarvam.test.ts +60 -0
- package/src/integrations/sarvam/types.ts +27 -0
- package/src/integrations/zoho/attach.ts +342 -0
- package/src/integrations/zoho/config.ts +125 -0
- package/src/integrations/zoho/index.ts +27 -0
- package/src/integrations/zoho/types.ts +21 -0
- package/src/integrations/zoho/zoho.test.ts +50 -0
- package/src/kit/clipboard-image.ts +107 -0
- package/src/kit/external-editor.ts +48 -0
- package/src/kit/image.ts +59 -0
- package/src/kit/index.ts +51 -0
- package/src/kit/shell.ts +19 -0
- package/src/kit/tool-fetch.ts +85 -0
- package/src/launch/catalog.ts +148 -0
- package/src/launch/contract.ts +187 -0
- package/src/launch/credentials.ts +625 -0
- package/src/launch/index.ts +98 -0
- package/src/launch/invocation/attachments.ts +179 -0
- package/src/launch/invocation/flags.ts +196 -0
- package/src/launch/invocation/index.ts +25 -0
- package/src/launch/invocation/read.ts +260 -0
- package/src/launch/invocation/usage.ts +67 -0
- package/src/launch/login.ts +324 -0
- package/src/launch/oauth.test.ts +18 -0
- package/src/launch/oauth.ts +203 -0
- package/src/launch/packages.ts +194 -0
- package/src/launch/pickers.ts +189 -0
- package/src/runtime-bridge/bridges/_drive.ts +96 -0
- package/src/runtime-bridge/bridges/builtins.ts +68 -0
- package/src/runtime-bridge/bridges/claude-cli.ts +123 -0
- package/src/runtime-bridge/bridges/codex-cli.ts +142 -0
- package/src/runtime-bridge/bridges/index.ts +33 -0
- package/src/runtime-bridge/bridges/indusagi-cli.ts +155 -0
- package/src/runtime-bridge/broker.ts +227 -0
- package/src/runtime-bridge/contract.ts +122 -0
- package/src/runtime-bridge/index.ts +79 -0
- package/src/runtime-bridge/sink.ts +180 -0
- package/src/sessions/contract.ts +81 -0
- package/src/sessions/index.ts +13 -0
- package/src/sessions/library.ts +229 -0
- package/src/settings/contract.ts +114 -0
- package/src/settings/index.ts +32 -0
- package/src/settings/manager.ts +117 -0
- package/src/transcript-export/index.ts +45 -0
- package/src/transcript-export/publish.ts +260 -0
- package/src/transcript-export/sgr.ts +315 -0
- package/src/transcript-export/template.ts +272 -0
- package/src/transcript-export/theme-bridge.ts +150 -0
- package/src/window-budget/budget/estimate.ts +135 -0
- package/src/window-budget/budget/gate.ts +33 -0
- package/src/window-budget/budget/index.ts +16 -0
- package/src/window-budget/budget/slice.ts +56 -0
- package/src/window-budget/condenser.ts +58 -0
- package/src/window-budget/contract.ts +184 -0
- package/src/window-budget/index.ts +19 -0
- package/src/window-budget/microcompact.ts +95 -0
- package/src/window-budget/rehydrate.ts +136 -0
- package/src/window-budget/summarize/condense.ts +103 -0
- package/src/window-budget/summarize/index.ts +14 -0
- package/src/window-budget/summarize/prompt.ts +149 -0
- package/src/workflow-engine/agent-runner.ts +181 -0
- package/src/workflow-engine/display.ts +224 -0
- package/src/workflow-engine/engine.ts +294 -0
- package/src/workflow-engine/index.ts +23 -0
- package/src/workflow-engine/parse.ts +172 -0
- package/src/workflow-engine/structured-output.ts +35 -0
- package/src/workspace/brand.ts +29 -0
- package/src/workspace/index.ts +18 -0
- package/src/workspace/locator.ts +103 -0
- package/src/workspace/runtime-detect.ts +64 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade registry — the ordered, idempotent catalog of one-time profile
|
|
3
|
+
* migrations.
|
|
4
|
+
*
|
|
5
|
+
* This module replaces a bag of magic constants and inlined `try/catch`
|
|
6
|
+
* migration blocks with a *registry*: an explicit, ordered list of named
|
|
7
|
+
* {@link Upgrade} steps. Each step owns a stable {@link Upgrade.id}, a one-line
|
|
8
|
+
* {@link Upgrade.describe}, and an idempotent {@link Upgrade.apply}. The driver
|
|
9
|
+
* ({@link "./apply"}) folds over the list in order, recording the ids it has run
|
|
10
|
+
* in a marker file so a step never runs twice — re-running the whole registry on
|
|
11
|
+
* an already-upgraded profile is a no-op.
|
|
12
|
+
*
|
|
13
|
+
* Idempotence contract (every step must honor it):
|
|
14
|
+
* - Detect "already done" cheaply and return early without side effects.
|
|
15
|
+
* - Treat a *missing* source (the thing being migrated from) as success — the
|
|
16
|
+
* profile is either fresh or already past this point.
|
|
17
|
+
* - Never destroy data: relocations move-or-skip, never overwrite a live
|
|
18
|
+
* destination; the legacy source is suffixed, not deleted.
|
|
19
|
+
*
|
|
20
|
+
* The seeded steps fold the legacy on-disk layout into the current one:
|
|
21
|
+
* 1. {@link foldCredentials} — legacy split auth → one secure `auth.json`.
|
|
22
|
+
* 2. {@link reshelveTranscripts} — loose session files → per-cwd `sessions/`.
|
|
23
|
+
* 3. {@link relocateBinaries} — managed `fd`/`rg` helpers → the `bin/` dir.
|
|
24
|
+
* 4. {@link renamePromptDir} — legacy `commands/` dir → `prompts/`.
|
|
25
|
+
*
|
|
26
|
+
* Session-directory encoding note: step 2 must place a transcript under the
|
|
27
|
+
* *same* per-cwd directory name the framework's session manager would compute.
|
|
28
|
+
* The framework publishes no standalone encoder for that name (the encoding is
|
|
29
|
+
* private to its session manager), so this module uses a clearly-named local
|
|
30
|
+
* helper, {@link projectTranscriptDirName}, rather than re-deriving the
|
|
31
|
+
* framework's private per-cwd directory encoding. If the framework later exports
|
|
32
|
+
* an encoder, swap the helper for the import without touching the step logic.
|
|
33
|
+
*/
|
|
34
|
+
import { promises as fs, constants as fsConstants } from "node:fs";
|
|
35
|
+
import { join, dirname, basename } from "node:path";
|
|
36
|
+
import type { Workspace } from "../contract.js";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One ordered, idempotent profile-layout upgrade.
|
|
40
|
+
*
|
|
41
|
+
* An upgrade is pure data plus one async effect. The registry order is the
|
|
42
|
+
* apply order; the {@link id} is the durable key recorded in the marker file
|
|
43
|
+
* once {@link apply} completes, so it must be stable across releases (renaming an
|
|
44
|
+
* id re-runs the step). {@link apply} must be safe to invoke against a profile in
|
|
45
|
+
* any state — fresh, partially-migrated, or fully-migrated.
|
|
46
|
+
*/
|
|
47
|
+
export interface Upgrade {
|
|
48
|
+
/** Durable identifier recorded once this step has run. Never rename. */
|
|
49
|
+
readonly id: string;
|
|
50
|
+
/** One-line human summary for logs and `--verbose` output. */
|
|
51
|
+
readonly describe: string;
|
|
52
|
+
/**
|
|
53
|
+
* Perform the upgrade against the resolved {@link Workspace}. Must be
|
|
54
|
+
* idempotent: detect "already applied" and return without side effects.
|
|
55
|
+
*
|
|
56
|
+
* @param ws The resolved, absolute on-disk layout.
|
|
57
|
+
*/
|
|
58
|
+
apply(ws: Workspace): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const SECURE_FILE_MODE = 384;
|
|
62
|
+
const RETIRED_SUFFIX = ".retired";
|
|
63
|
+
|
|
64
|
+
async function pathExists(target: string): Promise<boolean> {
|
|
65
|
+
try {
|
|
66
|
+
await fs.access(target, fsConstants.F_OK);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function isDirectory(target: string): Promise<boolean> {
|
|
74
|
+
try {
|
|
75
|
+
return (await fs.stat(target)).isDirectory();
|
|
76
|
+
} catch {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function isFile(target: string): Promise<boolean> {
|
|
82
|
+
try {
|
|
83
|
+
return (await fs.stat(target)).isFile();
|
|
84
|
+
} catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function readJsonOrUndefined(target: string): Promise<unknown> {
|
|
90
|
+
try {
|
|
91
|
+
return JSON.parse(await fs.readFile(target, "utf8"));
|
|
92
|
+
} catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function writeJsonWithMode(target: string, value: unknown, mode: number): Promise<void> {
|
|
98
|
+
await fs.mkdir(dirname(target), { recursive: true });
|
|
99
|
+
await fs.writeFile(target, `${JSON.stringify(value, null, 2)}\n`, { mode });
|
|
100
|
+
await fs.chmod(target, mode);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function relocateWithoutClobber(from: string, into: string): Promise<boolean> {
|
|
104
|
+
if (!(await pathExists(from))) return false;
|
|
105
|
+
if (await pathExists(into)) return false;
|
|
106
|
+
await fs.mkdir(dirname(into), { recursive: true });
|
|
107
|
+
await fs.rename(from, into);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Compute the per-project transcript sub-directory *name* for a working
|
|
113
|
+
* directory.
|
|
114
|
+
*
|
|
115
|
+
* This deliberately does not reproduce the framework session manager's private
|
|
116
|
+
* encoding character-for-character; the framework exposes no public encoder to
|
|
117
|
+
* import, so this is the documented local fallback. It produces a single,
|
|
118
|
+
* filesystem-safe directory leaf that is stable for a given absolute `cwd`:
|
|
119
|
+
* path separators, drive colons, and any other non-portable characters collapse
|
|
120
|
+
* to a single token, and a short content hash is appended so two distinct paths
|
|
121
|
+
* that happen to fold to the same token never collide.
|
|
122
|
+
*
|
|
123
|
+
* The output is used only when *relocating a loose legacy transcript* into the
|
|
124
|
+
* sessions tree; live sessions are always created by the framework itself, so
|
|
125
|
+
* this helper never competes with the framework for the canonical name.
|
|
126
|
+
*
|
|
127
|
+
* @param cwd Absolute working directory the transcript belongs to.
|
|
128
|
+
* @returns A safe directory leaf such as `proj-Users-me-app-1a2b3c4d`.
|
|
129
|
+
*/
|
|
130
|
+
export function projectTranscriptDirName(cwd: string): string {
|
|
131
|
+
const trimmed = cwd.trim() || "root";
|
|
132
|
+
const folded = trimmed.split(/[^A-Za-z0-9]+/u).filter((segment) => segment.length > 0).join("-");
|
|
133
|
+
const token = folded.length > 0 ? folded : "root";
|
|
134
|
+
return `proj-${token}-${shortPathHash(trimmed)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function shortPathHash(input: string): string {
|
|
138
|
+
let hash = 2166136261;
|
|
139
|
+
for (let i = 0; i < input.length; i++) {
|
|
140
|
+
hash ^= input.charCodeAt(i);
|
|
141
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
142
|
+
}
|
|
143
|
+
return hash.toString(16).padStart(8, "0");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const LEGACY_OAUTH_FILE = "oauth.json";
|
|
147
|
+
const LEGACY_SETTINGS_FILE = "settings.json";
|
|
148
|
+
const MANAGED_HELPER_BASENAMES = ["fd", "rg", "fd.exe", "rg.exe"];
|
|
149
|
+
const LEGACY_PROMPTS_DIRNAME = "commands";
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Fold a legacy split credential layout — a standalone `oauth.json` plus an
|
|
153
|
+
* `apiKeys` block embedded in `settings.json` — into the single consolidated
|
|
154
|
+
* `auth.json`, written with owner-only (`0o600`-equivalent) permissions.
|
|
155
|
+
*
|
|
156
|
+
* Idempotent: if `auth.json` already exists, nothing is touched. After a
|
|
157
|
+
* successful fold the legacy `oauth.json` is suffixed (`oauth.json.retired`) and
|
|
158
|
+
* the `apiKeys` block is removed from `settings.json`, so a second run finds no
|
|
159
|
+
* legacy sources and the existing `auth.json` short-circuits it regardless.
|
|
160
|
+
*/
|
|
161
|
+
export const foldCredentials: Upgrade = {
|
|
162
|
+
id: "fold-credentials-into-secure-auth-file",
|
|
163
|
+
describe: "Consolidate legacy oauth.json + settings.apiKeys into a 0600 auth.json",
|
|
164
|
+
async apply(ws) {
|
|
165
|
+
if (await pathExists(ws.authPath)) return;
|
|
166
|
+
const legacyOauthPath = join(ws.profileDir, LEGACY_OAUTH_FILE);
|
|
167
|
+
const legacySettingsPath = join(ws.profileDir, LEGACY_SETTINGS_FILE);
|
|
168
|
+
const oauth = await readJsonOrUndefined(legacyOauthPath);
|
|
169
|
+
const settings = await readJsonOrUndefined(legacySettingsPath);
|
|
170
|
+
const apiKeys = (settings as { apiKeys?: unknown } | undefined)?.apiKeys;
|
|
171
|
+
const haveOauth = oauth !== undefined && Object.keys(oauth as object).length > 0;
|
|
172
|
+
const haveApiKeys = apiKeys !== undefined && Object.keys(apiKeys as object).length > 0;
|
|
173
|
+
if (!haveOauth && !haveApiKeys) return;
|
|
174
|
+
const folded: Record<string, unknown> = {};
|
|
175
|
+
if (haveOauth) folded.oauth = oauth;
|
|
176
|
+
if (haveApiKeys) folded.apiKeys = apiKeys;
|
|
177
|
+
await writeJsonWithMode(ws.authPath, folded, SECURE_FILE_MODE);
|
|
178
|
+
if (await isFile(legacyOauthPath)) {
|
|
179
|
+
const retiredOauth = `${legacyOauthPath}${RETIRED_SUFFIX}`;
|
|
180
|
+
if (!(await pathExists(retiredOauth))) {
|
|
181
|
+
await fs.rename(legacyOauthPath, retiredOauth);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (haveApiKeys && settings && (await isFile(legacySettingsPath))) {
|
|
185
|
+
const { apiKeys: _migrated, ...remaining } = settings as { apiKeys?: unknown } & Record<string, unknown>;
|
|
186
|
+
await fs.writeFile(legacySettingsPath, `${JSON.stringify(remaining, null, 2)}\n`);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
async function readTranscriptHeader(filePath: string): Promise<{ type?: string; cwd?: string } | undefined> {
|
|
192
|
+
try {
|
|
193
|
+
const content = await fs.readFile(filePath, "utf8");
|
|
194
|
+
const firstLine = content.split("\n", 1)[0]?.trim();
|
|
195
|
+
if (!firstLine) return undefined;
|
|
196
|
+
return JSON.parse(firstLine);
|
|
197
|
+
} catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Relocate loose `*.jsonl` transcripts that were written directly under the
|
|
204
|
+
* profile root (a legacy layout) into the structured per-cwd sessions tree:
|
|
205
|
+
* `sessions/<projectDirName>/<file>`.
|
|
206
|
+
*
|
|
207
|
+
* A loose file is recognized as a transcript by a first-line header of
|
|
208
|
+
* `type: "session"`. The destination project directory is derived from the
|
|
209
|
+
* header's recorded `cwd` via {@link projectTranscriptDirName} (the framework
|
|
210
|
+
* owns no public encoder to delegate to — see this module's header). Each move
|
|
211
|
+
* is skip-on-clobber, so re-running never overwrites an already-relocated
|
|
212
|
+
* transcript, and once no loose transcripts remain the step is a pure no-op.
|
|
213
|
+
*/
|
|
214
|
+
export const reshelveTranscripts: Upgrade = {
|
|
215
|
+
id: "reshelve-loose-transcripts-into-sessions-dir",
|
|
216
|
+
describe: "Move loose session .jsonl files under the profile root into sessions/<cwd>/",
|
|
217
|
+
async apply(ws) {
|
|
218
|
+
let entries: string[];
|
|
219
|
+
try {
|
|
220
|
+
entries = await fs.readdir(ws.profileDir);
|
|
221
|
+
} catch {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
for (const entry of entries) {
|
|
225
|
+
if (!entry.endsWith(".jsonl")) continue;
|
|
226
|
+
const loosePath = join(ws.profileDir, entry);
|
|
227
|
+
if (!(await isFile(loosePath))) continue;
|
|
228
|
+
const header = await readTranscriptHeader(loosePath);
|
|
229
|
+
if (header?.type !== "session") continue;
|
|
230
|
+
const projectDir = projectTranscriptDirName(header.cwd ?? ws.profileDir);
|
|
231
|
+
const destination = join(ws.sessionsDir, projectDir, basename(loosePath));
|
|
232
|
+
await relocateWithoutClobber(loosePath, destination);
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Relocate the managed native helper binaries (`fd`, `rg`, and their Windows
|
|
239
|
+
* `.exe` forms) from the legacy `tools/` directory into the canonical managed
|
|
240
|
+
* `bin/` directory ({@link Workspace.binDir}).
|
|
241
|
+
*
|
|
242
|
+
* Each binary is moved skip-on-clobber and independently, so a partially-moved
|
|
243
|
+
* state self-heals on the next run and an already-relocated layout (or a fresh
|
|
244
|
+
* install with no legacy `tools/`) is a no-op.
|
|
245
|
+
*/
|
|
246
|
+
export const relocateBinaries: Upgrade = {
|
|
247
|
+
id: "relocate-managed-helper-binaries-to-bin",
|
|
248
|
+
describe: "Move managed fd/rg helper binaries from the legacy tools/ dir into bin/",
|
|
249
|
+
async apply(ws) {
|
|
250
|
+
const legacyToolsDir = join(ws.profileDir, "tools");
|
|
251
|
+
if (!(await isDirectory(legacyToolsDir))) return;
|
|
252
|
+
for (const name of MANAGED_HELPER_BASENAMES) {
|
|
253
|
+
const from = join(legacyToolsDir, name);
|
|
254
|
+
if (!(await isFile(from))) continue;
|
|
255
|
+
await relocateWithoutClobber(from, join(ws.binDir, name));
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Rename the legacy command-template directory (`commands/`) to the current
|
|
262
|
+
* prompt-template directory ({@link Workspace.promptsDir}).
|
|
263
|
+
*
|
|
264
|
+
* Idempotent: if the destination already exists the rename is skipped (the
|
|
265
|
+
* profile is already on the current layout), and if the legacy directory is
|
|
266
|
+
* absent there is nothing to do. The rename is whole-directory and
|
|
267
|
+
* non-destructive — a live `prompts/` is never replaced.
|
|
268
|
+
*/
|
|
269
|
+
export const renamePromptDir: Upgrade = {
|
|
270
|
+
id: "rename-legacy-commands-dir-to-prompts",
|
|
271
|
+
describe: "Rename the legacy commands/ template directory to prompts/",
|
|
272
|
+
async apply(ws) {
|
|
273
|
+
const legacyPromptsDir = join(ws.profileDir, LEGACY_PROMPTS_DIRNAME);
|
|
274
|
+
if (!(await isDirectory(legacyPromptsDir))) return;
|
|
275
|
+
await relocateWithoutClobber(legacyPromptsDir, ws.promptsDir);
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The ordered list of upgrades. Apply order is array order; ids are the durable
|
|
281
|
+
* marker keys. Append new steps to the end — never reorder or rename existing
|
|
282
|
+
* ids, as that would re-run already-applied migrations.
|
|
283
|
+
*/
|
|
284
|
+
export const UPGRADES: readonly Upgrade[] = [
|
|
285
|
+
foldCredentials,
|
|
286
|
+
reshelveTranscripts,
|
|
287
|
+
relocateBinaries,
|
|
288
|
+
renamePromptDir,
|
|
289
|
+
];
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentTool,
|
|
3
|
+
Briefing,
|
|
4
|
+
BriefingContext,
|
|
5
|
+
BriefingInputs,
|
|
6
|
+
BriefingSection,
|
|
7
|
+
} from "./contract.js";
|
|
8
|
+
|
|
9
|
+
export const TOOL_SUMMARIES: Readonly<Record<string, string>> = {
|
|
10
|
+
read: "Open a file's contents for inspection.",
|
|
11
|
+
write: "Create a new file or overwrite an existing one wholesale.",
|
|
12
|
+
edit: "Apply a precise in-place change by matching exact existing text.",
|
|
13
|
+
bash: "Run a shell command in the workspace.",
|
|
14
|
+
grep: "Search file contents by pattern across the tree.",
|
|
15
|
+
find: "Locate files and directories by name or glob.",
|
|
16
|
+
ls: "List the entries of a directory.",
|
|
17
|
+
task: "Hand a self-contained sub-task to a delegate agent.",
|
|
18
|
+
todoread: "Read back the current task checklist.",
|
|
19
|
+
todowrite: "Record or revise the task checklist.",
|
|
20
|
+
webfetch: "Retrieve and read the contents of a URL.",
|
|
21
|
+
websearch: "Query the web for current information.",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function joinBlocks(parts: readonly string[]): string {
|
|
25
|
+
return parts.filter((part) => part.trim().length > 0).join("\n\n");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function bullets(lines: readonly string[]): string {
|
|
29
|
+
return lines.map((line) => `- ${line}`).join("\n");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function escapeXml(value: string): string {
|
|
33
|
+
let out = "";
|
|
34
|
+
for (const ch of value) {
|
|
35
|
+
const escaped = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch];
|
|
36
|
+
out += escaped ?? ch;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function hasTool(ctx: BriefingContext, id: string): boolean {
|
|
42
|
+
return (ctx.tools ?? []).some((tool: AgentTool) => tool.name === id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function hasToolPrefix(ctx: BriefingContext, prefix: string): boolean {
|
|
46
|
+
return (ctx.tools ?? []).some((tool: AgentTool) => tool.name.startsWith(prefix));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function describeTool(tool: AgentTool): string {
|
|
50
|
+
const summary = TOOL_SUMMARIES[tool.name] ?? tool.description?.trim() ?? "";
|
|
51
|
+
return summary.length > 0 ? `\`${tool.name}\` — ${summary}` : `\`${tool.name}\``;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const ROLE_SECTION: BriefingSection = {
|
|
55
|
+
id: "role",
|
|
56
|
+
render: () => joinBlocks([
|
|
57
|
+
"You are a terminal-based software engineering assistant. You operate inside a real workspace and make progress by reading code, running commands, and editing files directly — not by describing what someone else should do.",
|
|
58
|
+
"Work toward the user's actual goal. Take the initiative to gather the context you need, but stay within the scope of what was asked: do not redesign, rename, or refactor beyond the request unless the user invites it.",
|
|
59
|
+
]),
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const TOOLS_SECTION: BriefingSection = {
|
|
63
|
+
id: "tools", title: "Tools",
|
|
64
|
+
applies: (ctx: BriefingContext) => (ctx.tools ?? []).length > 0,
|
|
65
|
+
render: (ctx: BriefingContext) => joinBlocks(["# Tools", "These capabilities are available to you this turn. Reach for the most specific one for the job.", bullets((ctx.tools ?? []).map(describeTool))]),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const GUIDELINES_SECTION: BriefingSection = {
|
|
69
|
+
id: "guidelines", title: "Working guidance",
|
|
70
|
+
render(ctx: BriefingContext) {
|
|
71
|
+
const items: string[] = [];
|
|
72
|
+
if (hasTool(ctx, "read")) items.push("Always inspect a file with the reader before you change it. Do not shell out to `cat`, `head`, or `sed` to view a file when the reader is available.");
|
|
73
|
+
if (hasTool(ctx, "edit")) items.push("For a targeted change, prefer the editor: it replaces an exact span of existing text, so the old text you supply must match the file byte-for-byte.");
|
|
74
|
+
if (hasTool(ctx, "write")) items.push("Reserve the writer for creating a new file or replacing one in full; for anything smaller, edit in place rather than rewriting the whole file.");
|
|
75
|
+
if (hasTool(ctx, "grep") || hasTool(ctx, "find") || hasTool(ctx, "ls")) items.push("Explore the tree with the dedicated search and listing tools rather than shell equivalents — they are quicker and already skip ignored paths.");
|
|
76
|
+
if (hasTool(ctx, "bash")) items.push("Use the shell for builds, tests, and other commands that no purpose-built tool covers. Keep commands scoped and avoid destructive operations unless the user asked for them.");
|
|
77
|
+
items.push("Keep your replies short and to the point. Lead with the result; add only the explanation the user needs to act on it.");
|
|
78
|
+
items.push("When a change is non-trivial, verify it — run the build or the relevant tests — before you report it as done.");
|
|
79
|
+
return joinBlocks(["# Working guidance", bullets(items)]);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const TASK_SECTION: BriefingSection = {
|
|
84
|
+
id: "tasks", title: "Task tracking",
|
|
85
|
+
applies: (ctx: BriefingContext) => hasTool(ctx, "todowrite") || hasTool(ctx, "todoread"),
|
|
86
|
+
render: () => joinBlocks(["# Task tracking", "For work that spans several steps, maintain a checklist with the task tools. Break the job into concrete items, mark exactly one in progress at a time, and tick items off as you finish them so the user can follow along."]),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const SUBAGENTS_SECTION: BriefingSection = {
|
|
90
|
+
id: "subagents", title: "Delegates",
|
|
91
|
+
applies: (ctx: BriefingContext) => (ctx.subagents ?? []).length > 0,
|
|
92
|
+
render: (ctx: BriefingContext) => joinBlocks(["# Delegates", "You can offload a focused, self-contained piece of work to one of these delegates with the task tool. Hand off when a sub-problem is well-scoped enough to be solved without your full conversation context.", bullets((ctx.subagents ?? []).map((subagent) => `**${subagent.name}** — ${subagent.purpose}.${subagent.when ? ` Use it when ${subagent.when}` : ""}`))]),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export const COORDINATOR_SECTION: BriefingSection = {
|
|
96
|
+
id: "coordinator", title: "Orchestrating sub-agents", applies: (ctx: BriefingContext) => hasTool(ctx, "task"),
|
|
97
|
+
render: () => joinBlocks([
|
|
98
|
+
"# Orchestrating sub-agents",
|
|
99
|
+
"When work is naturally parallel or context-heavy, you can offload bounded pieces to sub-agents via the `task` tool. Sub-agents execute autonomously and return one report.",
|
|
100
|
+
bullets([
|
|
101
|
+
"Delegate when a sub-problem is well-scoped enough to solve without your full conversation context, when multiple independent investigations can run in parallel, or when research would otherwise fill your context with raw output you won't need again.",
|
|
102
|
+
"Do NOT delegate trivial work, questions you can answer directly, or one quick file reads or shell commands — sub-agents add latency and lose the parent's accumulated reasoning.",
|
|
103
|
+
"Sub-agents receive a focused authoring tool deck: read, write, edit, grep, find, ls, bash, todoread, todowrite, webfetch, websearch, workflow, and structured_output. They do NOT see your conversation history and cannot spawn further sub-agents.",
|
|
104
|
+
"Each sub-agent runs its own complete tool loop and returns one final report. There is no built-in continue/cancel mechanism — plan the full objective upfront.",
|
|
105
|
+
"If a sub-agent fails or produces wrong output, do not retry the same task with the same arguments. Analyze the failure, adjust your approach, and spawn a fresh sub-agent with a refined objective.",
|
|
106
|
+
]),
|
|
107
|
+
"# Writing sub-agent objectives", "Workers cannot see your conversation. Every objective must be self-contained with everything the worker needs.",
|
|
108
|
+
bullets(["Include file paths, line numbers, error messages — workers start fresh and need complete context.", "State what 'done' looks like (commit hash, test result, report format).", "For implementation: 'Run relevant tests and typecheck, then commit your changes and report the hash.' — workers self-verify before reporting done.", "For research: 'Report findings — do not modify files.'", "Be precise about git operations — specify branch names, commit hashes, draft vs ready, reviewers.", "For implementation: 'Fix the root cause, not the symptom.' — guide workers toward durable fixes.", "For verification: 'Prove the code works, don't just confirm it exists.' and 'Try edge cases and error paths — don't just re-run what the implementation worker ran.'"]),
|
|
109
|
+
"# Phases for complex work", bullets(["Research — sub-agents (parallel). Investigate the codebase, find files, understand the problem. Each sub-agent reports findings without modifying files.", "Synthesis — you (parent). Read the findings. Understand the problem. Craft a concrete implementation spec with file paths, line numbers, and exact changes.", "Implementation — sub-agents. Make targeted changes per the spec. Run tests and typecheck. Commit. Report the hash.", "Verification — sub-agents. Test that the changes work end-to-end. Run the original failing scenario. Prove the fix, don't rubber-stamp."]),
|
|
110
|
+
"# Parallelism", "Launch independent sub-agents concurrently whenever possible. Don't serialize work that can run simultaneously. When researching, cover multiple angles in parallel — issue multiple task tool calls in a single message.",
|
|
111
|
+
]),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const PLAN_SECTION: BriefingSection = {
|
|
115
|
+
id: "plan-mode", title: "Plan mode", applies: (ctx: BriefingContext) => hasTool(ctx, "enter_plan_mode") || hasTool(ctx, "exit_plan_mode"),
|
|
116
|
+
render: () => joinBlocks(["# Plan mode", bullets(["For a task that needs investigation before any change, call `enter_plan_mode` first. That switches you into a read-only research phase: you may read, search, and inspect, but every file-mutating and shell tool is blocked until you leave.", "While in plan mode, gather all the context you need and draft a concrete plan. Do not try to edit files or run commands — those calls are rejected with a steering message.", "When the plan is ready, call `exit_plan_mode` with the full plan text. The user reviews it and either approves (you leave plan mode and carry the plan out) or rejects (you stay in plan mode to revise and re-propose). Use it to ask permission to start, not to do the work."])]),
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function taskSection(id: string, title: string, blocks: string[]): BriefingSection {
|
|
120
|
+
return { id, title, applies: (ctx: BriefingContext) => hasTool(ctx, "task"), render: () => joinBlocks([`# ${title}`, ...blocks]) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const PLAN_AGENT_SECTION = taskSection("plan-agent", "Plan sub-agent", ["For non-trivial implementation work you can delegate the entire planning phase to a read-only sub-agent that returns a structured plan:", "`task({ agent: \"plan\", objective: \"Plan the implementation of <feature>\" })`", "The plan sub-agent explores the codebase with read, grep, find, and ls; identifies existing patterns; designs an implementation approach with trade-offs; and returns a step-by-step plan with a 'Critical Files for Implementation' list. The sub-agent operates read-only — it cannot write, edit, or run state-changing commands. Use it before you start implementing when the scope is unclear or the change spans multiple files."]);
|
|
124
|
+
export const VERIFICATION_AGENT_SECTION = taskSection("verification-agent", "Verification sub-agent", ["For non-trivial implementation work, delegate independent verification to a dedicated sub-agent that adversarially tries to break the change:", "`task({ agent: \"verification\", objective: \"Verify the implementation of <feature>. Files changed: <list>. Approach: <summary>.\" })`", "The verification sub-agent runs the build, executes the test suite, runs linters/type-checkers, exercises edge cases (concurrency, boundary values, idempotency, orphan operations), and returns a structured verdict. It operates read-only on the project (temp test scripts allowed), ends with `VERDICT: PASS|FAIL|PARTIAL`, and never rubber-stamps work without running commands. Use it after implementation completes, before reporting success to the user."]);
|
|
125
|
+
export const EXPLORE_AGENT_SECTION = taskSection("explore-agent", "Explore sub-agent", ["For codebase questions that require fast parallel searching across many files, delegate to a dedicated read-only search sub-agent:", "`task({ agent: \"explore\", objective: \"Find all usages of <API>. Thoroughness: <quick|medium|very thorough>.\" })`", "The explore sub-agent runs file searches (find, grep), reads targeted files, and reports findings without modifying anything. It uses parallel tool calls for speed and adapts to your thoroughness request. Use it for broad codebase discovery, finding call sites, or answering 'how does X work in this repo?' questions."]);
|
|
126
|
+
|
|
127
|
+
export const SECURITY_REVIEW_SECTION: BriefingSection = { id: "security-review", title: "Security review", render: () => joinBlocks(["# Security review", "For non-trivial PRs, perform a focused security review with a senior security engineer mindset. Apply these filters:", bullets(["Only flag issues where you're >80% confident of actual exploitability.", "Skip theoretical issues, style concerns, and low-impact findings.", "Exclude DoS, secrets-on-disk, rate limiting, library version mismatches.", "Output: file, line, severity (HIGH/MEDIUM/LOW), category, description, exploit scenario, recommendation."]), "The full security-review prompt (with confidence scoring and 17 hard exclusions) is available as a constant in the bundled files for use via the task tool."]) };
|
|
128
|
+
export const CONNECTORS_SECTION: BriefingSection = { id: "connectors", title: "Connectors", applies: (ctx: BriefingContext) => hasToolPrefix(ctx, "connector_") || hasToolPrefix(ctx, "saas_"), render: () => joinBlocks(["# Connectors", bullets(["When a task needs an external service (issue trackers, calendars, docs, and the like), prefer the connector tools over scraping or guessing.", "Confirm the connection is authorized before you rely on it, and surface a clear next step if it is not.", "Page through large result sets deliberately rather than assuming the first response is complete."])]) };
|
|
129
|
+
export const PROJECT_CONTEXT_SECTION: BriefingSection = { id: "project-context", title: "Project context", applies: (ctx: BriefingContext) => (ctx.contextDocs ?? []).length > 0, render: (ctx: BriefingContext) => joinBlocks(["# Project context", "The following project documents describe conventions for this repository. Treat them as standing instructions.", ...(ctx.contextDocs ?? []).map((doc) => `## ${doc.path}\n\n${doc.body.trim()}`)]) };
|
|
130
|
+
export const SKILLS_SECTION: BriefingSection = { id: "skills", title: "Skills", applies: (ctx: BriefingContext) => (ctx.skills ?? []).length > 0, render: (ctx: BriefingContext) => joinBlocks(["# Skills", "Each skill below is a set of task-specific instructions stored on disk. When a task lines up with a skill's description, load its file with the reader and follow it. Paths a skill mentions are relative to that skill's own directory.", `<available_skills>\n${(ctx.skills ?? []).map((card) => ` <skill>\n <name>${escapeXml(card.name)}</name>\n <description>${escapeXml(card.description)}</description>\n <location>${escapeXml(card.location)}</location>\n </skill>`).join("\n")}\n</available_skills>`]) };
|
|
131
|
+
export const FOOTER_SECTION: BriefingSection = { id: "footer", render: (ctx: BriefingContext) => { const lines: string[] = []; const where = ctx.cwd ?? ctx.workspace; if (where) lines.push(`Working directory: ${where}`); lines.push(`Current time: ${(ctx.now ?? new Date()).toISOString()}`); return lines.join("\n"); } };
|
|
132
|
+
|
|
133
|
+
export const BRIEFING_SECTIONS: Briefing = [ROLE_SECTION, TOOLS_SECTION, GUIDELINES_SECTION, TASK_SECTION, SUBAGENTS_SECTION, COORDINATOR_SECTION, PLAN_SECTION, PLAN_AGENT_SECTION, VERIFICATION_AGENT_SECTION, EXPLORE_AGENT_SECTION, SECURITY_REVIEW_SECTION, CONNECTORS_SECTION, PROJECT_CONTEXT_SECTION, SKILLS_SECTION, FOOTER_SECTION];
|
|
134
|
+
|
|
135
|
+
export function isInputs(value: BriefingContext | BriefingInputs): value is BriefingInputs {
|
|
136
|
+
return "sections" in value && "context" in value;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function composeBriefing(input: BriefingContext | BriefingInputs): string {
|
|
140
|
+
const inputs = isInputs(input) ? input : { sections: BRIEFING_SECTIONS, context: input };
|
|
141
|
+
const rendered: string[] = [];
|
|
142
|
+
if (inputs.prelude?.trim()) rendered.push(inputs.prelude.trim());
|
|
143
|
+
for (const section of inputs.sections) {
|
|
144
|
+
if (section.applies && !section.applies(inputs.context)) continue;
|
|
145
|
+
const fragment = section.render(inputs.context).trim();
|
|
146
|
+
if (fragment) rendered.push(fragment);
|
|
147
|
+
}
|
|
148
|
+
if (inputs.append?.trim()) rendered.push(inputs.append.trim());
|
|
149
|
+
return joinBlocks(rendered);
|
|
150
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, extname, isAbsolute, join, parse, resolve } from "node:path";
|
|
3
|
+
import type { ContextDoc } from "./contract.js";
|
|
4
|
+
|
|
5
|
+
export interface GatherContextDocsOptions { readonly maxImportDepth?: number; readonly maxBytesPerDoc?: number; }
|
|
6
|
+
export const DEFAULT_MAX_IMPORT_DEPTH = 5;
|
|
7
|
+
export const DEFAULT_MAX_BYTES_PER_DOC = 40_000;
|
|
8
|
+
export const CANDIDATE_FILES = ["AGENTS.md", "CLAUDE.md", "INDUSAGI.md", "CLAUDE.local.md", "AGENTS.local.md", join(".indusagi", "CLAUDE.md"), join(".claude", "CLAUDE.md")];
|
|
9
|
+
export const TEXT_FILE_EXTENSIONS = new Set([".md", ".markdown", ".txt", ".text", ".json", ".jsonc", ".yaml", ".yml", ".toml", ".xml", ".csv", ".html", ".htm", ".css", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs", ".mts", ".cts", ".py", ".rb", ".go", ".rs", ".sh", ".env"]);
|
|
10
|
+
export const IMPORT_REGEX = /(?:^|\s)@((?:[^\s\\]|\\ )+)/g;
|
|
11
|
+
|
|
12
|
+
export function shortenPath(path: string, cwd: string, home: string): string { if (cwd && path.startsWith(cwd)) { const rest = path.slice(cwd.length).replace(/^[/\\]+/, ""); return rest ? `./${rest}` : "."; } return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path; }
|
|
13
|
+
export function dirChain(cwd: string): string[] { const chain: string[] = []; let current = cwd; for (let i = 0; i < 256; i++) { chain.push(current); const parent = dirname(current); if (parent === current || current === parse(current).root) break; current = parent; } return chain.reverse(); }
|
|
14
|
+
export function safeRealpath(path: string): string { try { return realpathSync(path); } catch { return path; } }
|
|
15
|
+
export function isTextFile(path: string): boolean { const extension = extname(path).toLowerCase(); return !extension || TEXT_FILE_EXTENSIONS.has(extension); }
|
|
16
|
+
export function readFileBody(path: string): string | null { if (!existsSync(path)) return null; try { const body = readFileSync(path, "utf8").trim(); return body || null; } catch { return null; } }
|
|
17
|
+
export function extractImports(body: string, baseDir: string, home: string): string[] { const out: string[] = []; IMPORT_REGEX.lastIndex = 0; let match: RegExpExecArray | null; while ((match = IMPORT_REGEX.exec(body))) { let spec = match[1]; if (!spec) continue; const hash = spec.indexOf("#"); if (hash >= 0) spec = spec.slice(0, hash); spec = spec.replace(/\\ /g, " ").trim(); if (!spec) continue; const absolute = spec.startsWith("~/") ? join(home, spec.slice(2)) : isAbsolute(spec) ? spec : resolve(baseDir, spec); if (isTextFile(absolute)) out.push(absolute); } return out; }
|
|
18
|
+
export function loadDocTree(path: string, cwd: string, home: string, seen: Set<string>, depth: number, maxDepth: number, maxBytes: number, out: ContextDoc[]): void { const real = safeRealpath(path); if (seen.has(real)) return; seen.add(real); const body = readFileBody(path); if (body === null) return; out.push({ path: shortenPath(path, cwd, home), body: body.length > maxBytes ? body.slice(0, maxBytes) : body }); if (depth >= maxDepth) return; for (const child of extractImports(body, dirname(path), home)) loadDocTree(child, cwd, home, seen, depth + 1, maxDepth, maxBytes, out); }
|
|
19
|
+
export function gatherContextDocs(cwd: string, home: string, opts?: GatherContextDocsOptions): readonly ContextDoc[] { const out: ContextDoc[] = []; try { const seen = new Set<string>(); const roots = dirChain(cwd); if (home) roots.push(home); for (const dir of roots) for (const name of CANDIDATE_FILES) loadDocTree(join(dir, name), cwd, home, seen, 0, opts?.maxImportDepth ?? DEFAULT_MAX_IMPORT_DEPTH, opts?.maxBytesPerDoc ?? DEFAULT_MAX_BYTES_PER_DOC, out); } catch { return out; } return out; }
|