@zerwiz/ymir 0.1.15 → 0.1.17
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/.claude/settings.json +31 -0
- package/.codex/hooks.json +43 -0
- package/.cursor/hooks.json +33 -0
- package/.pi/extensions/README.md +39 -0
- package/.pi/extensions/gna-pi-watch.ts +15 -0
- package/.pi/extensions/lib/ro-assistant-layout.ts +96 -0
- package/.pi/extensions/lib/ro-operational-user-layout.ts +145 -0
- package/.pi/extensions/lib/ro-visibility.ts +99 -0
- package/.pi/extensions/lib/ro-working-ship.ts +243 -0
- package/.pi/extensions/lib/rodd-operational-input.ts +62 -0
- package/.pi/extensions/lib/skuld-branch-dispatch.ts +252 -0
- package/.pi/extensions/lib/skuld-branch-model-picker.ts +77 -0
- package/.pi/extensions/lib/vordr-sessionstart-supervisor.mjs +48 -0
- package/.pi/extensions/ro.ts +11 -0
- package/.pi/extensions/skuld-branch-supervision.ts +11 -0
- package/.pi/extensions/syn-turnend-guard.ts +11 -0
- package/.pi/mcp.json +16 -0
- package/.pi/mcp.json.example +11 -0
- package/.pi/npm/.gitignore +2 -0
- package/.pi/settings.json +8 -0
- package/.pi/shared/extensions/gna-pi-watch.ts +750 -0
- package/.pi/shared/extensions/herdr-agent-state.ts +257 -0
- package/.pi/shared/extensions/open-editor.ts +509 -0
- package/.pi/shared/extensions/ro.ts +504 -0
- package/.pi/shared/extensions/skuld-branch-supervision.ts +1535 -0
- package/.pi/shared/extensions/syn-turnend-guard.ts +683 -0
- package/.pi/shared/extensions/todo.ts +133 -0
- package/.pi/shared/extensions/ymir-subagents.ts +287 -0
- package/.pi/shared/extensions/ymir-well.ts +130 -0
- package/CHANGELOG.md +8 -0
- package/assets/Ymir.md +4654 -0
- package/assets/healer.html +717 -0
- package/assets/icon-family/family-sheet.png +0 -0
- package/assets/icon-family/hlidskjalf.svg +1 -0
- package/assets/icon-family/sessrumnir.svg +1 -0
- package/assets/icon-family/smidja.svg +1 -0
- package/assets/icon-family/ymir.svg +1 -0
- package/assets/mimirswell.html +880 -0
- package/assets/mock/README.md +16 -0
- package/assets/mock/feeds.ts +175 -0
- package/assets/mock/mock.ts +408 -0
- package/assets/reference/docs/agent-control.md +122 -0
- package/assets/reference/docs/apodex/Appdex_as_Orchestrator_Planner_Reviewer_Assessment.md +176 -0
- package/assets/reference/docs/apodex/Final Report(1).md +198 -0
- package/assets/reference/docs/apodex/Final Report(2).md +54 -0
- package/assets/reference/docs/apodex/Final Report(3).md +40 -0
- package/assets/reference/docs/apodex/Final Report(4).md +234 -0
- package/assets/reference/docs/apodex/Final Report(5).md +13 -0
- package/assets/reference/docs/apodex/Final Report.md +82 -0
- package/assets/reference/docs/architecture.md +392 -0
- package/assets/reference/docs/arm-pretool-check.md +247 -0
- package/assets/reference/docs/calm-mode-feasibility.md +541 -0
- package/assets/reference/docs/calm.md +59 -0
- package/assets/reference/docs/captain-hold-lifecycle.md +96 -0
- package/assets/reference/docs/cd-guard.md +165 -0
- package/assets/reference/docs/cmux-backend.md +133 -0
- package/assets/reference/docs/codex-app-backend.md +57 -0
- package/assets/reference/docs/configuration.md +924 -0
- package/assets/reference/docs/documentation-audiences.json +468 -0
- package/assets/reference/docs/documentation-audiences.md +28 -0
- package/assets/reference/docs/examples/crew-dispatch.json +26 -0
- package/assets/reference/docs/examples/process-event-extension/file-signal.mjs +96 -0
- package/assets/reference/docs/examples/process-event-extension/firstmate-extension.json +15 -0
- package/assets/reference/docs/examples/watched-tools.json +24 -0
- package/assets/reference/docs/examples/wedge-alarm +22 -0
- package/assets/reference/docs/extension-bindings.md +237 -0
- package/assets/reference/docs/fm-test-isolation-proof.json +37 -0
- package/assets/reference/docs/fm-test-isolation-proof.md +149 -0
- package/assets/reference/docs/fm-test-portable-shards.md +118 -0
- package/assets/reference/docs/gitlab-merge-watch.md +275 -0
- package/assets/reference/docs/herdr-backend.md +346 -0
- package/assets/reference/docs/orca-backend.md +84 -0
- package/assets/reference/docs/pi-supervision-branch-poster.svg +125 -0
- package/assets/reference/docs/pi-supervision-branch.md +96 -0
- package/assets/reference/docs/plans/agentic-engineering-workflow.md +395 -0
- package/assets/reference/docs/plans/agentic-factory-implementation-plan.md +615 -0
- package/assets/reference/docs/plans/command-factory-enhancement-plan.md +420 -0
- package/assets/reference/docs/plans/phase-1-layer3-program-design.md +134 -0
- package/assets/reference/docs/plans/phase-2-vertical-slices.md +100 -0
- package/assets/reference/docs/plans/phase-3-measurable-goals.md +106 -0
- package/assets/reference/docs/plans/phase-4-context-budget.md +106 -0
- package/assets/reference/docs/plans/phase-5-pre-mortem-confidence.md +100 -0
- package/assets/reference/docs/plans/phase-6-incident-to-agent.md +109 -0
- package/assets/reference/docs/remote-secondmates.md +268 -0
- package/assets/reference/docs/scripts.md +146 -0
- package/assets/reference/docs/sessionstart-nudge.md +115 -0
- package/assets/reference/docs/subagent-guard.md +383 -0
- package/assets/reference/docs/supervision-protocols/claude.md +27 -0
- package/assets/reference/docs/supervision-protocols/codex.md +16 -0
- package/assets/reference/docs/supervision-protocols/cursor.md +31 -0
- package/assets/reference/docs/supervision-protocols/grok.md +39 -0
- package/assets/reference/docs/supervision-protocols/opencode.md +17 -0
- package/assets/reference/docs/supervision-protocols/pi.md +33 -0
- package/assets/reference/docs/supervision-protocols/unknown.md +13 -0
- package/assets/reference/docs/tmux-backend.md +115 -0
- package/assets/reference/docs/trace-context.md +117 -0
- package/assets/reference/docs/turnend-guard.md +170 -0
- package/assets/reference/docs/verification/dispatch-auth.md +204 -0
- package/assets/reference/docs/verification/muse.md +221 -0
- package/assets/reference/docs/verification/process-event-sources.md +208 -0
- package/assets/reference/docs/verification/public-followup.md +119 -0
- package/assets/reference/docs/verification/runtime-backends.md +1022 -0
- package/assets/reference/docs/verification/stow-memory.md +53 -0
- package/assets/reference/docs/verification/supervision.md +534 -0
- package/assets/reference/docs/verification/trace-context.md +31 -0
- package/assets/reference/docs/voice-relay.md +295 -0
- package/assets/reference/docs/watcher-continuity.md +119 -0
- package/assets/reference/docs/wedge-alarm.md +39 -0
- package/assets/reference/docs/zellij-backend.md +114 -0
- package/assets/reference/index.html +1172 -0
- package/assets/reference/skills/afk/SKILL.md +241 -0
- package/assets/reference/skills/ahoy/SKILL.md +56 -0
- package/assets/reference/skills/ask-user-authority/SKILL.md +56 -0
- package/assets/reference/skills/bearings/SKILL.md +161 -0
- package/assets/reference/skills/bearings/assets/board-template.html +731 -0
- package/assets/reference/skills/bootstrap-diagnostics/SKILL.md +68 -0
- package/assets/reference/skills/captain-hold-lifecycle/SKILL.md +54 -0
- package/assets/reference/skills/decision-hold-lifecycle/SKILL.md +15 -0
- package/assets/reference/skills/diagnostic-reasoning/SKILL.md +53 -0
- package/assets/reference/skills/firstmate-codexapp/SKILL.md +110 -0
- package/assets/reference/skills/firstmate-codexapp/agents/openai.yaml +4 -0
- package/assets/reference/skills/firstmate-coding-guidelines/SKILL.md +127 -0
- package/assets/reference/skills/firstmate-orca/SKILL.md +92 -0
- package/assets/reference/skills/fmx-respond/SKILL.md +283 -0
- package/assets/reference/skills/harness-adapters/SKILL.md +95 -0
- package/assets/reference/skills/harness-adapters/references/common/control-and-recovery.md +37 -0
- package/assets/reference/skills/harness-adapters/references/common/dispatch.md +32 -0
- package/assets/reference/skills/harness-adapters/references/common/model-and-effort.md +42 -0
- package/assets/reference/skills/harness-adapters/references/common/primary-hooks.md +40 -0
- package/assets/reference/skills/harness-adapters/references/harness/claude.md +55 -0
- package/assets/reference/skills/harness-adapters/references/harness/codex.md +43 -0
- package/assets/reference/skills/harness-adapters/references/harness/cursor.md +75 -0
- package/assets/reference/skills/harness-adapters/references/harness/grok.md +69 -0
- package/assets/reference/skills/harness-adapters/references/harness/kimi.md +51 -0
- package/assets/reference/skills/harness-adapters/references/harness/muse.md +70 -0
- package/assets/reference/skills/harness-adapters/references/harness/opencode.md +42 -0
- package/assets/reference/skills/harness-adapters/references/harness/pi.md +56 -0
- package/assets/reference/skills/process-event-sources/SKILL.md +137 -0
- package/assets/reference/skills/project-management/SKILL.md +92 -0
- package/assets/reference/skills/quota-array-dispatch/SKILL.md +131 -0
- package/assets/reference/skills/secondmate-provisioning/SKILL.md +254 -0
- package/assets/reference/skills/stow/SKILL.md +309 -0
- package/assets/reference/skills/stuck-crewmate-recovery/SKILL.md +54 -0
- package/assets/reference/skills/updatefirstmate/SKILL.md +64 -0
- package/assets/skills/README.md +5 -0
- package/assets/yggdrasil.html +649 -0
- package/assets/ymir-banner-01.png +0 -0
- package/assets/ymir-banner-02.png +0 -0
- package/assets/ymir-banner-03.png +0 -0
- package/assets/ymir-banner-04.png +0 -0
- package/assets/ymir-banner-05.png +0 -0
- package/assets/ymir-banner-06.png +0 -0
- package/assets/ymir-emblem-darksteel.svg +63 -0
- package/assets/ymir-emblem-forged.svg +55 -0
- package/assets/ymir-emblem-slate.svg +55 -0
- package/assets/ymir-mark-algiz-anvil.svg +9 -0
- package/assets/ymir-stave.svg +53 -0
- package/assets/ymir.html +563 -0
- package/data/learnings.md +33 -0
- package/data/local-models.md +34 -0
- package/data/operator.md +15 -0
- package/data/projects.md +28 -0
- package/data/realm.md +1 -0
- package/deploy/Containerfile +27 -0
- package/deploy/README.md +104 -0
- package/deploy/compose/compose.yaml +45 -0
- package/deploy/env.example +30 -0
- package/deploy/quadlet/ymir.container +57 -0
- package/package.json +11 -3
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open Editor
|
|
3
|
+
*
|
|
4
|
+
* Opens files from the current working directory in the user's default
|
|
5
|
+
* terminal editor. Resolves the editor from $VISUAL → $EDITOR → vi.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* /edit [path] Slash command with tab-completion over cwd files.
|
|
9
|
+
* No path → opens the current working directory
|
|
10
|
+
* (like `nvim .` / `code .`).
|
|
11
|
+
* ctrl+shift+e Keyboard shortcut → file picker over cwd.
|
|
12
|
+
*
|
|
13
|
+
* Inside Þjazi (herdr) the editor is seated in its OWN tab of the home
|
|
14
|
+
* workspace — focused, labelled for the file, recorded for close-all —
|
|
15
|
+
* so this session keeps its TUI and its agent alive. Outside herdr the
|
|
16
|
+
* old roads remain: terminal editors (nvim, vim, …) suspend the pi TUI
|
|
17
|
+
* and resume on exit; GUI editors (code, cursor, …) launch detached.
|
|
18
|
+
*
|
|
19
|
+
* Strictly user-facing — no LLM-callable tool is registered; agents
|
|
20
|
+
* have built-in `read` and `edit` already.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
24
|
+
import { appendFileSync, mkdirSync, readdirSync, statSync } from "node:fs";
|
|
25
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
26
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
27
|
+
import type { AutocompleteItem } from "@mariozechner/pi-tui";
|
|
28
|
+
|
|
29
|
+
// Editors that open in their own window / don't occupy the terminal.
|
|
30
|
+
// We launch these detached so pi keeps running.
|
|
31
|
+
const GUI_EDITORS = new Set([
|
|
32
|
+
"code",
|
|
33
|
+
"code-insiders",
|
|
34
|
+
"cursor",
|
|
35
|
+
"windsurf",
|
|
36
|
+
"subl",
|
|
37
|
+
"sublime_text",
|
|
38
|
+
"mate",
|
|
39
|
+
"atom",
|
|
40
|
+
"gedit",
|
|
41
|
+
"kate",
|
|
42
|
+
"zed",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
// Editors tried in order when neither $VISUAL nor $EDITOR names one that exists.
|
|
46
|
+
// Ordered by fitness for a terminal session: GUI first (detached, never blocks
|
|
47
|
+
// pi), then the common terminal editors, with plain `vi` last.
|
|
48
|
+
const FALLBACK_EDITORS = [
|
|
49
|
+
"code",
|
|
50
|
+
"cursor",
|
|
51
|
+
"zed",
|
|
52
|
+
"subl",
|
|
53
|
+
"nvim",
|
|
54
|
+
"vim",
|
|
55
|
+
"hx",
|
|
56
|
+
"helix",
|
|
57
|
+
"nano",
|
|
58
|
+
"micro",
|
|
59
|
+
"emacs",
|
|
60
|
+
"vi",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// Directories skipped by the non-git file walk fallback.
|
|
64
|
+
const EXCLUDED_DIRS = new Set([
|
|
65
|
+
"node_modules",
|
|
66
|
+
".git",
|
|
67
|
+
".hg",
|
|
68
|
+
".svn",
|
|
69
|
+
"dist",
|
|
70
|
+
"build",
|
|
71
|
+
".next",
|
|
72
|
+
".nuxt",
|
|
73
|
+
"target",
|
|
74
|
+
".venv",
|
|
75
|
+
"venv",
|
|
76
|
+
"__pycache__",
|
|
77
|
+
".tox",
|
|
78
|
+
".mypy_cache",
|
|
79
|
+
".pytest_cache",
|
|
80
|
+
".cache",
|
|
81
|
+
".turbo",
|
|
82
|
+
".parcel-cache",
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
const MAX_WALK_ENTRIES = 5000;
|
|
86
|
+
const LIST_CACHE_MS = 2000;
|
|
87
|
+
const MAX_COMPLETIONS = 50;
|
|
88
|
+
|
|
89
|
+
interface ResolvedEditor {
|
|
90
|
+
command: string;
|
|
91
|
+
argv: string[];
|
|
92
|
+
isGui: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function resolveEditor(): ResolvedEditor {
|
|
96
|
+
for (const raw of [process.env.VISUAL, process.env.EDITOR, "vi"]) {
|
|
97
|
+
if (!raw || !raw.trim()) continue;
|
|
98
|
+
// The configured command must actually exist: this extension runs on hosts
|
|
99
|
+
// that are not Omarchy, where `$EDITOR` may name a binary that is absent
|
|
100
|
+
// (Omarchy's own `omarchy-launch-editor`, or a bare `vi` on a box that only
|
|
101
|
+
// has `nvim`). Never spawn into an ENOENT — fall through to a real editor.
|
|
102
|
+
const parts = raw.trim().split(/\s+/).filter(Boolean);
|
|
103
|
+
const command = parts[0] || "";
|
|
104
|
+
if (!command) continue;
|
|
105
|
+
const found = findOnPath(command);
|
|
106
|
+
if (!found) continue;
|
|
107
|
+
const argv = parts.slice(1);
|
|
108
|
+
const isGui = GUI_EDITORS.has(basename(found).toLowerCase());
|
|
109
|
+
return { command: found, argv, isGui };
|
|
110
|
+
}
|
|
111
|
+
// Nothing configured and nothing found: pick the first editor that exists.
|
|
112
|
+
for (const candidate of FALLBACK_EDITORS) {
|
|
113
|
+
const found = findOnPath(candidate);
|
|
114
|
+
if (found) {
|
|
115
|
+
return { command: found, argv: [], isGui: GUI_EDITORS.has(candidate) };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Truly nothing: keep the historical shape so the caller's error is familiar.
|
|
119
|
+
return { command: "vi", argv: [], isGui: false };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Resolve <name> on PATH, returning the absolute path or null. Handles a name
|
|
123
|
+
// that is already a path (`/usr/bin/nvim`).
|
|
124
|
+
function findOnPath(name: string): string | null {
|
|
125
|
+
if (name.includes("/")) {
|
|
126
|
+
try {
|
|
127
|
+
statSync(name);
|
|
128
|
+
return name;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const dirs = (process.env.PATH || "").split(":").filter(Boolean);
|
|
134
|
+
for (const dir of dirs) {
|
|
135
|
+
try {
|
|
136
|
+
const full = join(dir, name);
|
|
137
|
+
const st = statSync(full);
|
|
138
|
+
if (st.isFile()) return full;
|
|
139
|
+
} catch {
|
|
140
|
+
// try the next directory
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
147
|
+
// File listing (git ls-files → bounded walk fallback) with small cache
|
|
148
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
interface CacheEntry {
|
|
151
|
+
at: number;
|
|
152
|
+
files: string[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const listCache = new Map<string, CacheEntry>();
|
|
156
|
+
const listInflight = new Map<string, Promise<string[]>>();
|
|
157
|
+
|
|
158
|
+
function walkFiles(cwd: string): string[] {
|
|
159
|
+
const out: string[] = [];
|
|
160
|
+
const stack: string[] = [""];
|
|
161
|
+
while (stack.length > 0 && out.length < MAX_WALK_ENTRIES) {
|
|
162
|
+
const rel = stack.pop()!;
|
|
163
|
+
const abs = rel ? join(cwd, rel) : cwd;
|
|
164
|
+
let entries;
|
|
165
|
+
try {
|
|
166
|
+
entries = readdirSync(abs, { withFileTypes: true });
|
|
167
|
+
} catch {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
for (const e of entries) {
|
|
171
|
+
if (out.length >= MAX_WALK_ENTRIES) break;
|
|
172
|
+
const entryRel = rel ? `${rel}/${e.name}` : e.name;
|
|
173
|
+
if (e.isDirectory()) {
|
|
174
|
+
if (EXCLUDED_DIRS.has(e.name)) continue;
|
|
175
|
+
stack.push(entryRel);
|
|
176
|
+
} else if (e.isFile()) {
|
|
177
|
+
out.push(entryRel);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out.sort();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function listFiles(pi: ExtensionAPI, cwd: string): Promise<string[]> {
|
|
185
|
+
const cached = listCache.get(cwd);
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
if (cached && now - cached.at < LIST_CACHE_MS) return cached.files;
|
|
188
|
+
|
|
189
|
+
const inflight = listInflight.get(cwd);
|
|
190
|
+
if (inflight) return inflight;
|
|
191
|
+
|
|
192
|
+
const p = (async (): Promise<string[]> => {
|
|
193
|
+
let files: string[] = [];
|
|
194
|
+
try {
|
|
195
|
+
const res = await pi.exec(
|
|
196
|
+
"git",
|
|
197
|
+
["ls-files", "-co", "--exclude-standard"],
|
|
198
|
+
{ timeout: 2000, cwd },
|
|
199
|
+
);
|
|
200
|
+
if (res.code === 0 && res.stdout) {
|
|
201
|
+
files = res.stdout.split("\n").map((s) => s.trim()).filter(Boolean).sort();
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// fall through to the walk
|
|
205
|
+
}
|
|
206
|
+
// git misses ignored dotfiles (.env.local, .env.realm, …); the walk
|
|
207
|
+
// already includes hidden entries — merge it in so the picker and the
|
|
208
|
+
// tab-completion can open them too. Tracked/visible first, bounded.
|
|
209
|
+
const seen = new Set(files);
|
|
210
|
+
for (const entry of walkFiles(cwd)) {
|
|
211
|
+
if (!seen.has(entry)) {
|
|
212
|
+
files.push(entry);
|
|
213
|
+
seen.add(entry);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
files.sort();
|
|
217
|
+
listCache.set(cwd, { at: Date.now(), files });
|
|
218
|
+
return files;
|
|
219
|
+
})();
|
|
220
|
+
|
|
221
|
+
listInflight.set(cwd, p);
|
|
222
|
+
try {
|
|
223
|
+
return await p;
|
|
224
|
+
} finally {
|
|
225
|
+
listInflight.delete(cwd);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Synchronous accessor for tab-completion. Kicks off async refresh if cold. */
|
|
230
|
+
function listFilesSync(pi: ExtensionAPI, cwd: string): string[] | null {
|
|
231
|
+
const cached = listCache.get(cwd);
|
|
232
|
+
if (cached && Date.now() - cached.at < LIST_CACHE_MS) return cached.files;
|
|
233
|
+
// Prime cache in background; return whatever we have (possibly stale).
|
|
234
|
+
void listFiles(pi, cwd);
|
|
235
|
+
return cached ? cached.files : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
239
|
+
// Þjazi (herdr): the editor keeps its own tab — never this hall
|
|
240
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
interface HerdrEnv {
|
|
243
|
+
available: boolean;
|
|
244
|
+
bin: string;
|
|
245
|
+
session: string;
|
|
246
|
+
workspace: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function herdrEnv(): HerdrEnv {
|
|
250
|
+
return {
|
|
251
|
+
available:
|
|
252
|
+
process.env.HERDR_ENV === "1" &&
|
|
253
|
+
!!process.env.HERDR_SOCKET_PATH &&
|
|
254
|
+
!!process.env.HERDR_BIN_PATH,
|
|
255
|
+
bin: process.env.HERDR_BIN_PATH || "herdr",
|
|
256
|
+
session: process.env.HERDR_SESSION || "default",
|
|
257
|
+
workspace: process.env.HERDR_WORKSPACE_ID || "",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function runHerdr(
|
|
262
|
+
env: HerdrEnv,
|
|
263
|
+
args: string[],
|
|
264
|
+
timeoutMs = 15000,
|
|
265
|
+
): Promise<{ code: number | null; stdout: string; stderr: string }> {
|
|
266
|
+
return new Promise((resolveResult) => {
|
|
267
|
+
const child = spawn(env.bin, ["--session", env.session, ...args], {
|
|
268
|
+
env: process.env,
|
|
269
|
+
});
|
|
270
|
+
let stdout = "";
|
|
271
|
+
let stderr = "";
|
|
272
|
+
child.stdout?.on("data", (d: Buffer) => (stdout += String(d)));
|
|
273
|
+
child.stderr?.on("data", (d: Buffer) => (stderr += String(d)));
|
|
274
|
+
child.on("error", (err: Error) =>
|
|
275
|
+
resolveResult({ code: null, stdout, stderr: err.message }),
|
|
276
|
+
);
|
|
277
|
+
child.on("close", (code) => resolveResult({ code, stdout, stderr }));
|
|
278
|
+
const timer = setTimeout(() => {
|
|
279
|
+
child.kill("SIGKILL");
|
|
280
|
+
resolveResult({ code: null, stdout, stderr: "herdr timed out" });
|
|
281
|
+
}, timeoutMs);
|
|
282
|
+
timer.unref?.();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Seat the editor in a fresh tab of the home workspace, labelled for the file
|
|
287
|
+
// and focused so it appears before the Allfather. The pane owns the editor;
|
|
288
|
+
// this hall keeps its TUI and its agent. Never blocks on the editor itself.
|
|
289
|
+
async function openInHerdrTab(
|
|
290
|
+
ctx: ExtensionContext,
|
|
291
|
+
env: HerdrEnv,
|
|
292
|
+
editor: ResolvedEditor,
|
|
293
|
+
absTarget: string,
|
|
294
|
+
openCwd: boolean,
|
|
295
|
+
): Promise<void> {
|
|
296
|
+
const label = openCwd
|
|
297
|
+
? "ymir:edit"
|
|
298
|
+
: `ymir:edit:${basename(absTarget).slice(0, 24)}`;
|
|
299
|
+
const paneCwd = openCwd ? absTarget : dirname(absTarget);
|
|
300
|
+
const editorTarget = openCwd ? "." : absTarget;
|
|
301
|
+
|
|
302
|
+
const createArgs = [
|
|
303
|
+
"tab",
|
|
304
|
+
"create",
|
|
305
|
+
"--cwd",
|
|
306
|
+
paneCwd,
|
|
307
|
+
"--label",
|
|
308
|
+
label,
|
|
309
|
+
"--focus",
|
|
310
|
+
];
|
|
311
|
+
if (env.workspace) {
|
|
312
|
+
createArgs.push("--workspace", env.workspace);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const created = await runHerdr(env, createArgs);
|
|
316
|
+
let tabId = "";
|
|
317
|
+
let paneId = "";
|
|
318
|
+
try {
|
|
319
|
+
const doc = JSON.parse(created.stdout) as {
|
|
320
|
+
result?: { tab?: { tab_id?: string }; root_pane?: { pane_id?: string } };
|
|
321
|
+
};
|
|
322
|
+
const r = doc.result ?? {};
|
|
323
|
+
tabId = r.tab?.tab_id ?? "";
|
|
324
|
+
paneId = r.root_pane?.pane_id ?? "";
|
|
325
|
+
} catch {
|
|
326
|
+
// parse failure falls through to the error branch below
|
|
327
|
+
}
|
|
328
|
+
if (!tabId || !paneId) {
|
|
329
|
+
ctx.ui.notify(
|
|
330
|
+
`herdr could not seat the editor tab: ${created.stderr || created.stdout || "unknown"}`,
|
|
331
|
+
"error",
|
|
332
|
+
);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Record the seat so `bin/herdr-run.sh close-all` can clear it.
|
|
337
|
+
try {
|
|
338
|
+
const root = process.env.BROKK_HOME || process.cwd();
|
|
339
|
+
const state =
|
|
340
|
+
process.env.BROKK_STATE_OVERRIDE || join(root, "state");
|
|
341
|
+
mkdirSync(state, { recursive: true });
|
|
342
|
+
appendFileSync(join(state, "herdr-seats"), `${tabId}\t${label}\n`);
|
|
343
|
+
} catch {
|
|
344
|
+
// bookkeeping is best-effort
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Fire the editor into the pane and return at once. `pane run` sends the
|
|
348
|
+
// command line plus Enter; the pane's interactive shell owns it from there.
|
|
349
|
+
const run = spawn(
|
|
350
|
+
env.bin,
|
|
351
|
+
[
|
|
352
|
+
"--session",
|
|
353
|
+
env.session,
|
|
354
|
+
"pane",
|
|
355
|
+
"run",
|
|
356
|
+
paneId,
|
|
357
|
+
editor.command,
|
|
358
|
+
...editor.argv,
|
|
359
|
+
editorTarget,
|
|
360
|
+
],
|
|
361
|
+
{ env: process.env, detached: true, stdio: "ignore" },
|
|
362
|
+
);
|
|
363
|
+
run.on("error", (err: Error) => {
|
|
364
|
+
ctx.ui.notify(`Editor failed in herdr tab ${tabId}: ${err.message}`, "error");
|
|
365
|
+
});
|
|
366
|
+
run.unref();
|
|
367
|
+
|
|
368
|
+
ctx.ui.notify(
|
|
369
|
+
`Opened ${openCwd ? "cwd" : basename(absTarget)} in herdr tab ${tabId}`,
|
|
370
|
+
"info",
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
375
|
+
// Open + picker
|
|
376
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
async function openFile(ctx: ExtensionContext, relPath: string): Promise<void> {
|
|
379
|
+
const absPath = resolve(ctx.cwd, relPath);
|
|
380
|
+
let isDir = false;
|
|
381
|
+
try {
|
|
382
|
+
const s = statSync(absPath);
|
|
383
|
+
if (s.isDirectory()) {
|
|
384
|
+
isDir = true;
|
|
385
|
+
} else if (!s.isFile()) {
|
|
386
|
+
ctx.ui.notify(`Not a file or directory: ${relPath}`, "error");
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
} catch {
|
|
390
|
+
ctx.ui.notify(`Path not found: ${relPath}`, "error");
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const openCwd = relPath === "." || isDir;
|
|
394
|
+
|
|
395
|
+
const editor = resolveEditor();
|
|
396
|
+
|
|
397
|
+
// Inside Þjazi the editor keeps its own tab; this hall stays alive.
|
|
398
|
+
const herdr = herdrEnv();
|
|
399
|
+
if (herdr.available) {
|
|
400
|
+
await openInHerdrTab(ctx, herdr, editor, absPath, openCwd);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (editor.isGui) {
|
|
405
|
+
try {
|
|
406
|
+
const child = spawn(editor.command, [...editor.argv, absPath], {
|
|
407
|
+
detached: true,
|
|
408
|
+
stdio: "ignore",
|
|
409
|
+
env: process.env,
|
|
410
|
+
cwd: ctx.cwd,
|
|
411
|
+
});
|
|
412
|
+
child.on("error", (err) => {
|
|
413
|
+
ctx.ui.notify(`Editor failed: ${err.message}`, "error");
|
|
414
|
+
});
|
|
415
|
+
child.unref();
|
|
416
|
+
ctx.ui.notify(`Opened ${relPath || "."} in ${editor.command}`, "info");
|
|
417
|
+
} catch (err) {
|
|
418
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
419
|
+
ctx.ui.notify(`Failed to launch ${editor.command}: ${msg}`, "error");
|
|
420
|
+
}
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Terminal editor: suspend TUI, run blocking, resume.
|
|
425
|
+
if (!ctx.hasUI) {
|
|
426
|
+
ctx.ui.notify("Terminal editor requires an interactive TUI", "error");
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const exitCode = await ctx.ui.custom<number | null>((tui, _theme, _kb, done) => {
|
|
431
|
+
tui.stop();
|
|
432
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
433
|
+
let status: number | null = null;
|
|
434
|
+
let spawnError: Error | null = null;
|
|
435
|
+
try {
|
|
436
|
+
const result = spawnSync(editor.command, [...editor.argv, absPath], {
|
|
437
|
+
stdio: "inherit",
|
|
438
|
+
env: process.env,
|
|
439
|
+
cwd: ctx.cwd,
|
|
440
|
+
});
|
|
441
|
+
if (result.error) spawnError = result.error;
|
|
442
|
+
status = result.status;
|
|
443
|
+
} catch (err) {
|
|
444
|
+
spawnError = err instanceof Error ? err : new Error(String(err));
|
|
445
|
+
}
|
|
446
|
+
tui.start();
|
|
447
|
+
tui.requestRender(true);
|
|
448
|
+
if (spawnError) {
|
|
449
|
+
// Surface after TUI comes back so the toast renders.
|
|
450
|
+
queueMicrotask(() =>
|
|
451
|
+
ctx.ui.notify(`Failed to launch ${editor.command}: ${spawnError!.message}`, "error"),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
done(status);
|
|
455
|
+
return { render: () => [], invalidate: () => {} };
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
if (exitCode != null && exitCode !== 0) {
|
|
459
|
+
ctx.ui.notify(`${editor.command} exited with code ${exitCode}`, "warning");
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function pickAndOpen(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
464
|
+
const files = await listFiles(pi, ctx.cwd);
|
|
465
|
+
if (files.length === 0) {
|
|
466
|
+
ctx.ui.notify("No files found in cwd", "warning");
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const choice = await ctx.ui.select("Open file", files);
|
|
470
|
+
if (!choice) return;
|
|
471
|
+
await openFile(ctx, choice);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
475
|
+
// Extension entrypoint
|
|
476
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
export default function (pi: ExtensionAPI) {
|
|
479
|
+
// Warm the file list cache on session start so the first tab-complete
|
|
480
|
+
// and the first ctrl+e feel instant.
|
|
481
|
+
pi.on("session_start", (_event, ctx) => {
|
|
482
|
+
void listFiles(pi, ctx.cwd);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
pi.registerCommand("edit", {
|
|
486
|
+
description: "Open a file in $VISUAL/$EDITOR (no arg = picker)",
|
|
487
|
+
getArgumentCompletions: (prefix: string): AutocompleteItem[] | null => {
|
|
488
|
+
// Best-effort synchronous lookup; primes cache in background.
|
|
489
|
+
// ctx.cwd not available here — fall back to process.cwd().
|
|
490
|
+
const files = listFilesSync(pi, process.cwd());
|
|
491
|
+
if (!files) return null;
|
|
492
|
+
const matches = files.filter((f) => f.startsWith(prefix)).slice(0, MAX_COMPLETIONS);
|
|
493
|
+
if (matches.length === 0) return null;
|
|
494
|
+
return matches.map((value) => ({ value, label: value }));
|
|
495
|
+
},
|
|
496
|
+
handler: async (args, ctx) => {
|
|
497
|
+
const target = (args ?? "").trim();
|
|
498
|
+
// No arg: open cwd in the editor (like `nvim .` / `code .`).
|
|
499
|
+
await openFile(ctx, target === "" ? "." : target);
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
pi.registerShortcut("ctrl+shift+e", {
|
|
504
|
+
description: "Open file in $VISUAL/$EDITOR",
|
|
505
|
+
handler: async (ctx) => {
|
|
506
|
+
await pickAndOpen(pi, ctx);
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
}
|