@wrongstack/cli 1.0.28 → 1.0.29
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/data/providers.json +22 -0
- package/dist/{acp-M4565YUW.js → acp-QWCBMQSO.js} +2 -2
- package/dist/{chunk-RGFWGUAB.js → chunk-4UWCMBPG.js} +159 -3
- package/dist/{chunk-ENRYW73H.js → chunk-6A7URMWN.js} +79 -8
- package/dist/{chunk-5JYDKG5B.js → chunk-BZHT5GPF.js} +6 -1
- package/dist/{chunk-L5TD7UIG.js → chunk-IFQ6NQPL.js} +4 -4
- package/dist/{chunk-3ZTLPNEQ.js → chunk-KT34LWIS.js} +9 -8
- package/dist/{chunk-UZ2TMHNH.js → chunk-UKP7H2U4.js} +10 -2
- package/dist/{chunk-EPR42SWM.js → chunk-WRYZLEXP.js} +20 -1
- package/dist/{cli-context-O2SHBDFH.js → cli-context-6RONY47P.js} +18 -18
- package/dist/{cli-main-BKG4ZVLN.js → cli-main-YS3HIOSD.js} +51 -41
- package/dist/{execution-THXWTHYT.js → execution-JEYF2L7O.js} +31 -23
- package/dist/fleet/host-explore-companion.d.ts +1 -1
- package/dist/{hq-44F4OQH2.js → hq-BNK67HOE.js} +3 -3
- package/dist/hq-publisher.d.ts +4 -1
- package/dist/hq-server/sage-sync-send.d.ts +5 -0
- package/dist/hq-server/server-lifecycle.d.ts +1 -1
- package/dist/hq-server/snapshot.d.ts +1 -1
- package/dist/{hq-server-UIB24LU6.js → hq-server-PCQACV7Y.js} +2 -2
- package/dist/hq-ttl-N4DAIAVX.js +7 -0
- package/dist/index.js +3 -3
- package/dist/{mailbox-serve-GNGXEQ2R.js → mailbox-serve-FW3JCCPK.js} +2 -2
- package/dist/{per-subcommand-help-QDAQASPZ.js → per-subcommand-help-MWQWFIAV.js} +2 -2
- package/dist/sage-FFKUOCQL.js +475 -0
- package/dist/sage-connect.d.ts +62 -0
- package/dist/sage-hq-sync.d.ts +8 -0
- package/dist/sage-sync-ZMTHB477.js +69 -0
- package/dist/{sessions-config-XHLXJ5C3.js → sessions-config-JGIZRNT6.js} +15 -4
- package/dist/{short-circuit-flags-BSIPTAPD.js → short-circuit-flags-5N2LPK4R.js} +3 -3
- package/dist/subcommands/handlers/helpers.d.ts +1 -1
- package/dist/subcommands/handlers/sage-sync.d.ts +4 -0
- package/dist/subcommands/handlers/sage.d.ts +5 -0
- package/dist/{subcommands-SRRB3RAS.js → subcommands-WX4GDMWB.js} +2 -2
- package/dist/{version-help-XJJQ7YSH.js → version-help-HN32QYQN.js} +2 -2
- package/dist/{webui-server-GCXIO3JY.js → webui-server-EXJWLB3X.js} +10 -8
- package/package.json +30 -29
- package/dist/hq-ttl-VJR65OBJ.js +0 -7
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import {
|
|
2
|
+
restoreFlags
|
|
3
|
+
} from "./chunk-CSAPOCBP.js";
|
|
4
|
+
|
|
5
|
+
// src/subcommands/handlers/sage.ts
|
|
6
|
+
import { accessSync, constants, readFileSync } from "node:fs";
|
|
7
|
+
import * as fs from "node:fs/promises";
|
|
8
|
+
import * as path2 from "node:path";
|
|
9
|
+
import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
|
|
10
|
+
|
|
11
|
+
// src/sage-connect.ts
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { toErrorMessage } from "@wrongstack/core/utils";
|
|
14
|
+
var SAGE_CONNECT_TARGETS = ["claude-code", "codex", "cursor", "antigravity"];
|
|
15
|
+
function isSageConnectTarget(value) {
|
|
16
|
+
return SAGE_CONNECT_TARGETS.includes(value);
|
|
17
|
+
}
|
|
18
|
+
function resolveSageLaunch(client, opts) {
|
|
19
|
+
const serveArgs = ["sage", "mcp", "--origin", client];
|
|
20
|
+
const command = opts.command?.trim() || "wstack";
|
|
21
|
+
const resolved = opts.findOnPath(command);
|
|
22
|
+
const platform = opts.platform ?? process.platform;
|
|
23
|
+
const shim = platform === "win32" && resolved !== null && /\.(cmd|bat)$/i.test(path.extname(resolved));
|
|
24
|
+
const launch = shim ? { command: "cmd", args: ["/c", command, ...serveArgs] } : { command, args: serveArgs };
|
|
25
|
+
if (resolved === null) {
|
|
26
|
+
return {
|
|
27
|
+
launch,
|
|
28
|
+
warning: `"${command}" is not on PATH, so ${client} will fail to start the server. Install wstack globally or pass --command <path to wstack>.`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { launch };
|
|
32
|
+
}
|
|
33
|
+
var MANAGED_NOTE = "<!-- Managed by `wstack sage connect`; `wstack sage disconnect` removes it. -->";
|
|
34
|
+
function layoutFor(target, skillName) {
|
|
35
|
+
switch (target) {
|
|
36
|
+
case "claude-code":
|
|
37
|
+
return {
|
|
38
|
+
mcpFile: ".mcp.json",
|
|
39
|
+
mcpFormat: "json",
|
|
40
|
+
guideFile: path.join(".claude", "skills", skillName, "SKILL.md"),
|
|
41
|
+
guideFormat: "skill",
|
|
42
|
+
notes: ["Claude Code asks once to approve project MCP servers from .mcp.json."]
|
|
43
|
+
};
|
|
44
|
+
case "codex":
|
|
45
|
+
return {
|
|
46
|
+
mcpFile: path.join(".codex", "config.toml"),
|
|
47
|
+
mcpFormat: "toml",
|
|
48
|
+
guideFile: path.join(".agents", "skills", skillName, "SKILL.md"),
|
|
49
|
+
guideFormat: "skill",
|
|
50
|
+
notes: [
|
|
51
|
+
"Codex reads a project .codex/config.toml only when the project is trusted; mark it trusted in Codex if the server does not appear."
|
|
52
|
+
]
|
|
53
|
+
};
|
|
54
|
+
case "cursor":
|
|
55
|
+
return {
|
|
56
|
+
mcpFile: path.join(".cursor", "mcp.json"),
|
|
57
|
+
mcpFormat: "json",
|
|
58
|
+
guideFile: path.join(".cursor", "rules", `${skillName}.mdc`),
|
|
59
|
+
guideFormat: "cursor-rule",
|
|
60
|
+
notes: ["Enable the server once in Cursor Settings > MCP if it starts disabled."]
|
|
61
|
+
};
|
|
62
|
+
case "antigravity":
|
|
63
|
+
return {
|
|
64
|
+
mcpFile: path.join(".agents", "mcp_config.json"),
|
|
65
|
+
mcpFormat: "json",
|
|
66
|
+
guideFile: path.join(".agents", "skills", skillName, "SKILL.md"),
|
|
67
|
+
guideFormat: "skill",
|
|
68
|
+
notes: [
|
|
69
|
+
"If the server does not appear, add the `wstack sage connect print` JSON to ~/.gemini/config/mcp_config.json (some Antigravity CLI builds load MCP servers only from the global file)."
|
|
70
|
+
]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function planSageConnect(target, opts) {
|
|
75
|
+
const layout = layoutFor(target, opts.guide.skillName);
|
|
76
|
+
const mcpPath = path.join(opts.projectRoot, layout.mcpFile);
|
|
77
|
+
const guidePath = path.join(opts.projectRoot, layout.guideFile);
|
|
78
|
+
const mcpBefore = opts.read(mcpPath);
|
|
79
|
+
const mcpAfter = layout.mcpFormat === "json" ? setJsonMcpServer(mcpBefore, opts.guide.serverName, jsonEntry(target, opts.launch), mcpPath) : setTomlMcpServer(mcpBefore, opts.guide.serverName, opts.launch);
|
|
80
|
+
const guideBefore = opts.read(guidePath);
|
|
81
|
+
const guideAfter = renderGuide(layout.guideFormat, opts.guide);
|
|
82
|
+
return {
|
|
83
|
+
target,
|
|
84
|
+
changes: [
|
|
85
|
+
change(mcpPath, mcpBefore, mcpAfter, `MCP server "${opts.guide.serverName}"`),
|
|
86
|
+
change(guidePath, guideBefore, guideAfter, guideSummary(layout.guideFormat))
|
|
87
|
+
],
|
|
88
|
+
notes: layout.notes
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function planSageDisconnect(target, opts) {
|
|
92
|
+
const layout = layoutFor(target, opts.guide.skillName);
|
|
93
|
+
const mcpPath = path.join(opts.projectRoot, layout.mcpFile);
|
|
94
|
+
const guidePath = path.join(opts.projectRoot, layout.guideFile);
|
|
95
|
+
const changes = [];
|
|
96
|
+
const mcpBefore = opts.read(mcpPath);
|
|
97
|
+
if (mcpBefore !== void 0) {
|
|
98
|
+
const after = layout.mcpFormat === "json" ? removeJsonMcpServer(mcpBefore, opts.guide.serverName, mcpPath) : removeTomlMcpServer(mcpBefore, opts.guide.serverName);
|
|
99
|
+
changes.push(removal(mcpPath, mcpBefore, after, `MCP server "${opts.guide.serverName}"`));
|
|
100
|
+
}
|
|
101
|
+
const guideBefore = opts.read(guidePath);
|
|
102
|
+
const sharedWith = SAGE_CONNECT_TARGETS.filter(
|
|
103
|
+
(other) => other !== target && layoutFor(other, opts.guide.skillName).guideFile === layout.guideFile && isSageConnected(other, opts)
|
|
104
|
+
);
|
|
105
|
+
if (guideBefore !== void 0) {
|
|
106
|
+
if (sharedWith.length > 0) {
|
|
107
|
+
changes.push({
|
|
108
|
+
path: guidePath,
|
|
109
|
+
action: "unchanged",
|
|
110
|
+
summary: `${guideSummary(layout.guideFormat)} (still used by ${sharedWith.join(", ")})`
|
|
111
|
+
});
|
|
112
|
+
} else if (guideBefore.includes(MANAGED_NOTE)) {
|
|
113
|
+
changes.push({
|
|
114
|
+
path: guidePath,
|
|
115
|
+
action: "delete",
|
|
116
|
+
summary: guideSummary(layout.guideFormat)
|
|
117
|
+
});
|
|
118
|
+
} else {
|
|
119
|
+
changes.push({
|
|
120
|
+
path: guidePath,
|
|
121
|
+
action: "unchanged",
|
|
122
|
+
summary: `${guideSummary(layout.guideFormat)} (edited by hand; left in place)`
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return { target, changes, notes: [] };
|
|
127
|
+
}
|
|
128
|
+
function change(absPath, before, after, summary) {
|
|
129
|
+
if (before === after) return { path: absPath, action: "unchanged", summary };
|
|
130
|
+
return {
|
|
131
|
+
path: absPath,
|
|
132
|
+
action: before === void 0 ? "create" : "update",
|
|
133
|
+
content: after,
|
|
134
|
+
summary
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function removal(absPath, before, after, summary) {
|
|
138
|
+
if (after === before)
|
|
139
|
+
return { path: absPath, action: "unchanged", summary: `${summary} (not present)` };
|
|
140
|
+
if (after.trim() === "" || after.trim() === "{}")
|
|
141
|
+
return { path: absPath, action: "delete", summary };
|
|
142
|
+
return { path: absPath, action: "update", content: after, summary };
|
|
143
|
+
}
|
|
144
|
+
function guideSummary(format) {
|
|
145
|
+
return format === "skill" ? "SAGE memory skill" : "SAGE memory rule";
|
|
146
|
+
}
|
|
147
|
+
function isSageConnected(target, opts) {
|
|
148
|
+
const layout = layoutFor(target, opts.guide.skillName);
|
|
149
|
+
const content = opts.read(path.join(opts.projectRoot, layout.mcpFile));
|
|
150
|
+
if (content === void 0) return false;
|
|
151
|
+
if (layout.mcpFormat === "toml") {
|
|
152
|
+
return content.split(/\r?\n/).some((line) => isOwnedTomlHeader(line, opts.guide.serverName));
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const servers = JSON.parse(content).mcpServers;
|
|
156
|
+
return typeof servers === "object" && servers !== null && opts.guide.serverName in servers;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function jsonEntry(target, launch) {
|
|
162
|
+
return target === "claude-code" ? { type: "stdio", command: launch.command, args: launch.args } : { command: launch.command, args: launch.args };
|
|
163
|
+
}
|
|
164
|
+
function parseJsonObject(content, absPath) {
|
|
165
|
+
if (content === void 0 || content.trim() === "") return {};
|
|
166
|
+
let parsed;
|
|
167
|
+
try {
|
|
168
|
+
parsed = JSON.parse(content);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`${absPath} is not valid JSON (${toErrorMessage(error)}); fix it by hand, nothing was changed.`
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
175
|
+
throw new Error(`${absPath} is not a JSON object; nothing was changed.`);
|
|
176
|
+
}
|
|
177
|
+
return parsed;
|
|
178
|
+
}
|
|
179
|
+
function serializeJson(value) {
|
|
180
|
+
return `${JSON.stringify(value, null, 2)}
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
function setJsonMcpServer(content, name, entry, absPath) {
|
|
184
|
+
const root = parseJsonObject(content, absPath);
|
|
185
|
+
const servers = root["mcpServers"];
|
|
186
|
+
if (servers !== void 0 && (typeof servers !== "object" || servers === null || Array.isArray(servers))) {
|
|
187
|
+
throw new Error(`${absPath}: "mcpServers" is not an object; nothing was changed.`);
|
|
188
|
+
}
|
|
189
|
+
const next = {
|
|
190
|
+
...root,
|
|
191
|
+
mcpServers: { ...servers ?? {}, [name]: entry }
|
|
192
|
+
};
|
|
193
|
+
const serialized = serializeJson(next);
|
|
194
|
+
if (content !== void 0 && serializeJson(root) === serialized) return content;
|
|
195
|
+
return serialized;
|
|
196
|
+
}
|
|
197
|
+
function removeJsonMcpServer(content, name, absPath) {
|
|
198
|
+
const root = parseJsonObject(content, absPath);
|
|
199
|
+
const servers = root["mcpServers"];
|
|
200
|
+
if (typeof servers !== "object" || servers === null || Array.isArray(servers) || !(name in servers)) {
|
|
201
|
+
return content;
|
|
202
|
+
}
|
|
203
|
+
const { [name]: _removed, ...rest } = servers;
|
|
204
|
+
const next = { ...root };
|
|
205
|
+
if (Object.keys(rest).length === 0) delete next["mcpServers"];
|
|
206
|
+
else next["mcpServers"] = rest;
|
|
207
|
+
return serializeJson(next);
|
|
208
|
+
}
|
|
209
|
+
function tomlString(value) {
|
|
210
|
+
return JSON.stringify(value);
|
|
211
|
+
}
|
|
212
|
+
function isOwnedTomlHeader(line, name) {
|
|
213
|
+
const header = line.trim();
|
|
214
|
+
const bare = `mcp_servers.${name}`;
|
|
215
|
+
const quoted = `mcp_servers."${name}"`;
|
|
216
|
+
return [bare, quoted].some((key) => header === `[${key}]` || header.startsWith(`[${key}.`));
|
|
217
|
+
}
|
|
218
|
+
function stripTomlTable(content, name) {
|
|
219
|
+
const lines = content.split(/\r?\n/);
|
|
220
|
+
const kept = [];
|
|
221
|
+
let skipping = false;
|
|
222
|
+
for (const line of lines) {
|
|
223
|
+
if (/^\s*\[/.test(line)) skipping = isOwnedTomlHeader(line, name);
|
|
224
|
+
if (!skipping) kept.push(line);
|
|
225
|
+
}
|
|
226
|
+
return kept.join("\n");
|
|
227
|
+
}
|
|
228
|
+
function setTomlMcpServer(content, name, launch) {
|
|
229
|
+
const table = [
|
|
230
|
+
`[mcp_servers.${name}]`,
|
|
231
|
+
`command = ${tomlString(launch.command)}`,
|
|
232
|
+
`args = [${launch.args.map(tomlString).join(", ")}]`
|
|
233
|
+
].join("\n");
|
|
234
|
+
const base = stripTomlTable(content ?? "", name).replace(/\s+$/, "");
|
|
235
|
+
const next = base ? `${base}
|
|
236
|
+
|
|
237
|
+
${table}
|
|
238
|
+
` : `${table}
|
|
239
|
+
`;
|
|
240
|
+
if (content !== void 0 && content.replace(/\r\n/g, "\n") === next) return content;
|
|
241
|
+
return next;
|
|
242
|
+
}
|
|
243
|
+
function removeTomlMcpServer(content, name) {
|
|
244
|
+
const stripped = stripTomlTable(content, name);
|
|
245
|
+
if (stripped === content.replace(/\r\n/g, "\n")) return content;
|
|
246
|
+
const trimmed = stripped.replace(/\s+$/, "");
|
|
247
|
+
return trimmed ? `${trimmed}
|
|
248
|
+
` : "";
|
|
249
|
+
}
|
|
250
|
+
function yamlString(value) {
|
|
251
|
+
return JSON.stringify(value);
|
|
252
|
+
}
|
|
253
|
+
function renderGuide(format, guide) {
|
|
254
|
+
const frontmatter = format === "skill" ? [`name: ${guide.skillName}`, `description: ${yamlString(guide.skillDescription)}`] : [`description: ${yamlString(guide.skillDescription)}`, "alwaysApply: false"];
|
|
255
|
+
return ["---", ...frontmatter, "---", "", guide.skillBody.trimEnd(), "", MANAGED_NOTE, ""].join(
|
|
256
|
+
"\n"
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
function renderSageConnectSnippets(launch, serverName) {
|
|
260
|
+
const json = JSON.stringify(
|
|
261
|
+
{ mcpServers: { [serverName]: { command: launch.command, args: launch.args } } },
|
|
262
|
+
null,
|
|
263
|
+
2
|
|
264
|
+
);
|
|
265
|
+
return [
|
|
266
|
+
"JSON (Claude Desktop, Cursor, Antigravity, Windsurf, most MCP clients):",
|
|
267
|
+
json,
|
|
268
|
+
"",
|
|
269
|
+
"TOML (Codex):",
|
|
270
|
+
`[mcp_servers.${serverName}]`,
|
|
271
|
+
`command = ${tomlString(launch.command)}`,
|
|
272
|
+
`args = [${launch.args.map(tomlString).join(", ")}]`,
|
|
273
|
+
"",
|
|
274
|
+
"Start the client from the project root (or set its working directory to it):",
|
|
275
|
+
"the server serves the memory of the WrongStack project it is launched in."
|
|
276
|
+
].join("\n");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/subcommands/handlers/sage.ts
|
|
280
|
+
var USAGE = [
|
|
281
|
+
"Usage:",
|
|
282
|
+
` wstack sage connect <${SAGE_CONNECT_TARGETS.join("|")}|all> [--dry-run] [--command <path>]`,
|
|
283
|
+
" wstack sage connect print",
|
|
284
|
+
` wstack sage disconnect <${SAGE_CONNECT_TARGETS.join("|")}|all> [--dry-run]`,
|
|
285
|
+
" wstack sage mcp [--origin <name>]",
|
|
286
|
+
" wstack sage sync [--restart-service]",
|
|
287
|
+
"",
|
|
288
|
+
"Connects this project's SAGE memory to another coding agent over MCP. The",
|
|
289
|
+
"server attaches to the SAGE daemon while wstack is open in this project; it",
|
|
290
|
+
"never starts one. Tools: memory_search, memory_for_file, memory_for_path,",
|
|
291
|
+
"memory_graph, and memory_candidates (list/propose only)."
|
|
292
|
+
].join("\n");
|
|
293
|
+
function optionValue(args, name) {
|
|
294
|
+
const flag = `--${name}`;
|
|
295
|
+
for (let i = 0; i < args.length; i++) {
|
|
296
|
+
const arg = args[i];
|
|
297
|
+
if (arg === flag) {
|
|
298
|
+
const next = args[i + 1];
|
|
299
|
+
return next !== void 0 && !next.startsWith("--") ? next : void 0;
|
|
300
|
+
}
|
|
301
|
+
if (arg.startsWith(`${flag}=`)) return arg.slice(flag.length + 1);
|
|
302
|
+
}
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
function positionals(args, valued) {
|
|
306
|
+
const out = [];
|
|
307
|
+
for (let i = 0; i < args.length; i++) {
|
|
308
|
+
const arg = args[i];
|
|
309
|
+
if (arg.startsWith("--")) {
|
|
310
|
+
if (valued.includes(arg.slice(2)) && !arg.includes("=")) i++;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
out.push(arg);
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
function findExecutableOnPath(cmd) {
|
|
318
|
+
const exists = (p) => {
|
|
319
|
+
try {
|
|
320
|
+
accessSync(p, process.platform === "win32" ? constants.F_OK : constants.X_OK);
|
|
321
|
+
return true;
|
|
322
|
+
} catch {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
if (cmd.includes("/") || cmd.includes("\\")) return exists(cmd) ? path2.resolve(cmd) : null;
|
|
327
|
+
const suffixes = process.platform === "win32" && path2.extname(cmd) === "" ? (process.env["PATHEXT"] ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
|
|
328
|
+
for (const dir of (process.env["PATH"] ?? "").split(path2.delimiter)) {
|
|
329
|
+
if (!dir) continue;
|
|
330
|
+
for (const suffix of suffixes) {
|
|
331
|
+
const candidate = path2.join(dir, cmd + suffix);
|
|
332
|
+
if (exists(candidate)) return candidate;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
function readIfExists(absPath) {
|
|
338
|
+
try {
|
|
339
|
+
return readFileSync(absPath, "utf8");
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error.code === "ENOENT") return void 0;
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async function applyChange(change2, projectRoot) {
|
|
346
|
+
if (change2.action === "delete") {
|
|
347
|
+
await fs.rm(change2.path, { force: true });
|
|
348
|
+
const root = path2.resolve(projectRoot);
|
|
349
|
+
let dir = path2.dirname(path2.resolve(change2.path));
|
|
350
|
+
while (dir !== root && dir.startsWith(root + path2.sep)) {
|
|
351
|
+
try {
|
|
352
|
+
await fs.rmdir(dir);
|
|
353
|
+
} catch {
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
dir = path2.dirname(dir);
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (change2.action === "create" || change2.action === "update") {
|
|
361
|
+
await fs.mkdir(path2.dirname(change2.path), { recursive: true });
|
|
362
|
+
await fs.writeFile(change2.path, change2.content ?? "", "utf8");
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function loadGuide() {
|
|
366
|
+
const mcp = await import("@wrongstack/sage-mcp");
|
|
367
|
+
return {
|
|
368
|
+
serverName: mcp.SAGE_MCP_SERVER_NAME,
|
|
369
|
+
skillName: mcp.SAGE_SKILL_NAME,
|
|
370
|
+
skillDescription: mcp.SAGE_SKILL_DESCRIPTION,
|
|
371
|
+
skillBody: mcp.SAGE_SKILL_BODY
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function targetsFrom(word) {
|
|
375
|
+
if (word === "all") return [...SAGE_CONNECT_TARGETS];
|
|
376
|
+
if (word && isSageConnectTarget(word)) return [word];
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
var sageCmd = async (rawArgs, deps) => {
|
|
380
|
+
const args = restoreFlags(rawArgs, deps, ["origin", "command", "dry-run", "restart-service"]);
|
|
381
|
+
const words = positionals(args, ["origin", "command"]);
|
|
382
|
+
const sub = words[0];
|
|
383
|
+
const out = (line) => deps.renderer.write(`${line}
|
|
384
|
+
`);
|
|
385
|
+
if (sub === "sync") {
|
|
386
|
+
const { runSageHqSyncCommand } = await import("./sage-sync-ZMTHB477.js");
|
|
387
|
+
return runSageHqSyncCommand(deps, args.includes("--restart-service"));
|
|
388
|
+
}
|
|
389
|
+
if (sub === "mcp") {
|
|
390
|
+
const client = optionValue(args, "origin")?.trim() || "mcp-client";
|
|
391
|
+
const { serveAttachedSageMcpStdio } = await import("@wrongstack/sage-mcp");
|
|
392
|
+
await serveAttachedSageMcpStdio({
|
|
393
|
+
projectRoot: deps.projectRoot,
|
|
394
|
+
directory: deps.config.Sage?.storage?.directory,
|
|
395
|
+
origin: client
|
|
396
|
+
});
|
|
397
|
+
return 0;
|
|
398
|
+
}
|
|
399
|
+
if (sub !== "connect" && sub !== "disconnect") {
|
|
400
|
+
out(USAGE);
|
|
401
|
+
return sub === void 0 || sub === "help" ? 0 : 1;
|
|
402
|
+
}
|
|
403
|
+
const guide = await loadGuide();
|
|
404
|
+
const dryRun = args.includes("--dry-run");
|
|
405
|
+
const command = optionValue(args, "command");
|
|
406
|
+
if (sub === "connect" && words[1] === "print") {
|
|
407
|
+
const { launch, warning } = resolveSageLaunch("mcp-client", {
|
|
408
|
+
command,
|
|
409
|
+
findOnPath: findExecutableOnPath
|
|
410
|
+
});
|
|
411
|
+
out(renderSageConnectSnippets(launch, guide.serverName));
|
|
412
|
+
if (warning) out(`
|
|
413
|
+
warning: ${warning}`);
|
|
414
|
+
return 0;
|
|
415
|
+
}
|
|
416
|
+
const targets = targetsFrom(words[1]);
|
|
417
|
+
if (!targets) {
|
|
418
|
+
out(USAGE);
|
|
419
|
+
return words[1] === void 0 ? 0 : 1;
|
|
420
|
+
}
|
|
421
|
+
let failed = false;
|
|
422
|
+
const warnings = /* @__PURE__ */ new Set();
|
|
423
|
+
out(
|
|
424
|
+
`${dryRun ? "Preview (nothing written)" : sub === "connect" ? "Connecting" : "Disconnecting"} SAGE memory \u2014 project ${deps.projectRoot}`
|
|
425
|
+
);
|
|
426
|
+
for (const target of targets) {
|
|
427
|
+
out(`
|
|
428
|
+
${target}:`);
|
|
429
|
+
try {
|
|
430
|
+
let plan;
|
|
431
|
+
if (sub === "connect") {
|
|
432
|
+
const { launch, warning } = resolveSageLaunch(target, {
|
|
433
|
+
command,
|
|
434
|
+
findOnPath: findExecutableOnPath
|
|
435
|
+
});
|
|
436
|
+
if (warning) warnings.add(warning);
|
|
437
|
+
plan = planSageConnect(target, {
|
|
438
|
+
projectRoot: deps.projectRoot,
|
|
439
|
+
launch,
|
|
440
|
+
guide,
|
|
441
|
+
read: readIfExists
|
|
442
|
+
});
|
|
443
|
+
} else {
|
|
444
|
+
plan = planSageDisconnect(target, {
|
|
445
|
+
projectRoot: deps.projectRoot,
|
|
446
|
+
guide,
|
|
447
|
+
read: readIfExists
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
if (plan.changes.length === 0) out(" nothing to remove");
|
|
451
|
+
for (const change2 of plan.changes) {
|
|
452
|
+
const rel = path2.relative(deps.projectRoot, change2.path) || change2.path;
|
|
453
|
+
out(` ${change2.action.padEnd(9)} ${rel} \u2014 ${change2.summary}`);
|
|
454
|
+
if (!dryRun) await applyChange(change2, deps.projectRoot);
|
|
455
|
+
}
|
|
456
|
+
if (sub === "connect") for (const note of plan.notes) out(` note: ${note}`);
|
|
457
|
+
} catch (error) {
|
|
458
|
+
failed = true;
|
|
459
|
+
deps.renderer.writeError(` ${target}: ${toErrorMessage2(error)}`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
for (const warning of warnings) out(`
|
|
463
|
+
warning: ${warning}`);
|
|
464
|
+
if (sub === "connect" && !dryRun && !failed) {
|
|
465
|
+
out(
|
|
466
|
+
"\nMemory is served while wstack is open in this project. Restart the client (or reload its MCP servers) to pick the server up."
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
return failed ? 1 : 0;
|
|
470
|
+
};
|
|
471
|
+
export {
|
|
472
|
+
findExecutableOnPath,
|
|
473
|
+
sageCmd
|
|
474
|
+
};
|
|
475
|
+
//# sourceMappingURL=sage-FFKUOCQL.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export declare const SAGE_CONNECT_TARGETS: readonly ['claude-code', 'codex', 'cursor', 'antigravity'];
|
|
2
|
+
export type SageConnectTarget = (typeof SAGE_CONNECT_TARGETS)[number];
|
|
3
|
+
export declare function isSageConnectTarget(value: string): value is SageConnectTarget;
|
|
4
|
+
/** The guide pieces, injected so this module stays free of the MCP package. */
|
|
5
|
+
export interface SageConnectGuide {
|
|
6
|
+
serverName: string;
|
|
7
|
+
skillName: string;
|
|
8
|
+
skillDescription: string;
|
|
9
|
+
skillBody: string;
|
|
10
|
+
}
|
|
11
|
+
export interface SageLaunch {
|
|
12
|
+
command: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface ResolveSageLaunchOptions {
|
|
16
|
+
/** `--command`: an explicit executable, used as written. */
|
|
17
|
+
command?: string | undefined;
|
|
18
|
+
platform?: NodeJS.Platform | undefined;
|
|
19
|
+
/** Resolve a bare command on PATH; `null` when absent. */
|
|
20
|
+
findOnPath: (cmd: string) => string | null;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The command a client runs. Bare `wstack` keeps project files portable
|
|
24
|
+
* between machines. On Windows an npm install is a `.cmd` shim, which a
|
|
25
|
+
* client spawning without a shell cannot start, so it goes through `cmd /c`.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveSageLaunch(client: string, opts: ResolveSageLaunchOptions): {
|
|
28
|
+
launch: SageLaunch;
|
|
29
|
+
warning?: string;
|
|
30
|
+
};
|
|
31
|
+
export type PlannedChangeAction = 'create' | 'update' | 'delete' | 'unchanged';
|
|
32
|
+
export interface PlannedChange {
|
|
33
|
+
/** Absolute path. */
|
|
34
|
+
path: string;
|
|
35
|
+
action: PlannedChangeAction;
|
|
36
|
+
/** New content for create/update. */
|
|
37
|
+
content?: string | undefined;
|
|
38
|
+
summary: string;
|
|
39
|
+
}
|
|
40
|
+
export interface SageConnectPlan {
|
|
41
|
+
target: SageConnectTarget;
|
|
42
|
+
changes: PlannedChange[];
|
|
43
|
+
notes: string[];
|
|
44
|
+
}
|
|
45
|
+
export interface PlanSageConnectOptions {
|
|
46
|
+
projectRoot: string;
|
|
47
|
+
launch: SageLaunch;
|
|
48
|
+
guide: SageConnectGuide;
|
|
49
|
+
/** Current content, `undefined` when the file does not exist. */
|
|
50
|
+
read: (absPath: string) => string | undefined;
|
|
51
|
+
}
|
|
52
|
+
export declare function planSageConnect(target: SageConnectTarget, opts: PlanSageConnectOptions): SageConnectPlan;
|
|
53
|
+
export declare function planSageDisconnect(target: SageConnectTarget, opts: Omit<PlanSageConnectOptions, 'launch'>): SageConnectPlan;
|
|
54
|
+
/** Whether `target`'s MCP file currently carries the SAGE server entry. */
|
|
55
|
+
export declare function isSageConnected(target: SageConnectTarget, opts: Pick<PlanSageConnectOptions, 'projectRoot' | 'guide' | 'read'>): boolean;
|
|
56
|
+
export declare function setJsonMcpServer(content: string | undefined, name: string, entry: Record<string, unknown>, absPath: string): string;
|
|
57
|
+
export declare function removeJsonMcpServer(content: string, name: string, absPath: string): string;
|
|
58
|
+
export declare function setTomlMcpServer(content: string | undefined, name: string, launch: SageLaunch): string;
|
|
59
|
+
export declare function removeTomlMcpServer(content: string, name: string): string;
|
|
60
|
+
/** `wstack sage connect print`: config for any MCP client, by hand. */
|
|
61
|
+
export declare function renderSageConnectSnippets(launch: SageLaunch, serverName: string): string;
|
|
62
|
+
//# sourceMappingURL=sage-connect.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type HqPublisher, type HqSageSnapshotPayload } from '@wrongstack/core/hq';
|
|
2
|
+
/** Uses the single SQLite owner over IPC, including writes from MCP and other hosts. */
|
|
3
|
+
export declare function createSageHqSync(projectRoot: string, publisher: HqPublisher, directory?: string): {
|
|
4
|
+
handleRemote(snapshot: HqSageSnapshotPayload): Promise<void>;
|
|
5
|
+
refresh: () => Promise<void>;
|
|
6
|
+
stop(): void;
|
|
7
|
+
};
|
|
8
|
+
//# sourceMappingURL=sage-hq-sync.d.ts.map
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
startCliHqConnection
|
|
3
|
+
} from "./chunk-4UWCMBPG.js";
|
|
4
|
+
import "./chunk-F5DCFAKI.js";
|
|
5
|
+
|
|
6
|
+
// src/subcommands/handlers/sage-sync.ts
|
|
7
|
+
import { resolveHqConfig } from "@wrongstack/core/hq";
|
|
8
|
+
import { SageProjectServerConnection } from "@wrongstack/sage";
|
|
9
|
+
async function runSageHqSyncCommand(deps, restartService, signal) {
|
|
10
|
+
if (signal?.aborted) return 0;
|
|
11
|
+
if (!resolveHqConfig({ config: deps.config.hq })?.enabled) {
|
|
12
|
+
deps.renderer.writeError("HQ is disabled. Enable HQ before starting SAGE synchronization.\n");
|
|
13
|
+
return 1;
|
|
14
|
+
}
|
|
15
|
+
if (restartService) {
|
|
16
|
+
const control = new SageProjectServerConnection(
|
|
17
|
+
deps.projectRoot,
|
|
18
|
+
deps.config.Sage?.storage?.directory,
|
|
19
|
+
{ spawnIfMissing: false }
|
|
20
|
+
);
|
|
21
|
+
try {
|
|
22
|
+
const result = await control.shutdown("operator-requested SAGE sync upgrade");
|
|
23
|
+
if (!result.stopped && result.reason !== "not-running") {
|
|
24
|
+
deps.renderer.writeError(`SAGE restart failed: ${result.reason ?? "unknown error"}
|
|
25
|
+
`);
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
} finally {
|
|
29
|
+
control.close();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (signal?.aborted) return 0;
|
|
33
|
+
const connection = startCliHqConnection({
|
|
34
|
+
clientKind: "cli",
|
|
35
|
+
projectRoot: deps.projectRoot,
|
|
36
|
+
appConfig: deps.config,
|
|
37
|
+
capabilities: ["telemetry.publish"],
|
|
38
|
+
ownKanbanSync: false,
|
|
39
|
+
ownSageSync: true
|
|
40
|
+
});
|
|
41
|
+
deps.renderer.write(
|
|
42
|
+
`SAGE HQ sync running for ${connection.getPublisher()?.project.projectId ?? deps.projectRoot}. Reconnect is automatic; Ctrl+C stops this bridge.
|
|
43
|
+
`
|
|
44
|
+
);
|
|
45
|
+
try {
|
|
46
|
+
await new Promise((resolve) => {
|
|
47
|
+
const keepAlive = setInterval(() => {
|
|
48
|
+
}, 6e4);
|
|
49
|
+
const stop = () => {
|
|
50
|
+
clearInterval(keepAlive);
|
|
51
|
+
process.removeListener("SIGINT", stop);
|
|
52
|
+
process.removeListener("SIGTERM", stop);
|
|
53
|
+
signal?.removeEventListener("abort", stop);
|
|
54
|
+
resolve();
|
|
55
|
+
};
|
|
56
|
+
process.once("SIGINT", stop);
|
|
57
|
+
process.once("SIGTERM", stop);
|
|
58
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
59
|
+
if (signal?.aborted) stop();
|
|
60
|
+
});
|
|
61
|
+
return 0;
|
|
62
|
+
} finally {
|
|
63
|
+
connection.stop();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export {
|
|
67
|
+
runSageHqSyncCommand
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=sage-sync-ZMTHB477.js.map
|
|
@@ -18,13 +18,24 @@ import { color as color2, expectDefined } from "@wrongstack/core/utils";
|
|
|
18
18
|
|
|
19
19
|
// src/subcommands/handlers/helpers.ts
|
|
20
20
|
import { isSecretField } from "@wrongstack/core/security";
|
|
21
|
-
function redactKeys(obj) {
|
|
21
|
+
function redactKeys(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
22
22
|
if (!obj || typeof obj !== "object") return obj;
|
|
23
|
-
if (
|
|
23
|
+
if (seen.has(obj)) return "[Circular]";
|
|
24
|
+
seen.add(obj);
|
|
25
|
+
if (Array.isArray(obj)) return obj.map((item) => redactKeys(item, seen));
|
|
24
26
|
const out = {};
|
|
25
27
|
for (const [k, v] of Object.entries(obj)) {
|
|
28
|
+
if (k === "__proto__") {
|
|
29
|
+
Object.defineProperty(out, k, {
|
|
30
|
+
value: redactKeys(v, seen),
|
|
31
|
+
writable: true,
|
|
32
|
+
enumerable: true,
|
|
33
|
+
configurable: true
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
26
37
|
if (isSecretField(k) && typeof v === "string" && v.length > 0) out[k] = "[REDACTED]";
|
|
27
|
-
else out[k] = redactKeys(v);
|
|
38
|
+
else out[k] = redactKeys(v, seen);
|
|
28
39
|
}
|
|
29
40
|
return out;
|
|
30
41
|
}
|
|
@@ -525,4 +536,4 @@ export {
|
|
|
525
536
|
configCmd,
|
|
526
537
|
sessionsCmd
|
|
527
538
|
};
|
|
528
|
-
//# sourceMappingURL=sessions-config-
|
|
539
|
+
//# sourceMappingURL=sessions-config-JGIZRNT6.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
handleHelpVersionShortCircuit
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-IFQ6NQPL.js";
|
|
4
|
+
import "./chunk-BZHT5GPF.js";
|
|
5
5
|
import "./chunk-F5DCFAKI.js";
|
|
6
6
|
import "./chunk-Q4INH2DG.js";
|
|
7
7
|
export {
|
|
8
8
|
handleHelpVersionShortCircuit
|
|
9
9
|
};
|
|
10
|
-
//# sourceMappingURL=short-circuit-flags-
|
|
10
|
+
//# sourceMappingURL=short-circuit-flags-5N2LPK4R.js.map
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function redactKeys(obj: unknown): unknown;
|
|
1
|
+
export declare function redactKeys(obj: unknown, seen?: WeakSet<object>): unknown;
|
|
2
2
|
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { SubcommandDeps } from '../contracts.js';
|
|
2
|
+
/** A memory-only HQ connection for headless hosts and upgrading active sessions. */
|
|
3
|
+
export declare function runSageHqSyncCommand(deps: SubcommandDeps, restartService: boolean, signal?: AbortSignal): Promise<number>;
|
|
4
|
+
//# sourceMappingURL=sage-sync.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { SubcommandHandler } from '../contracts.js';
|
|
2
|
+
/** PATH lookup with PATHEXT on Windows; an explicit path is checked as written. */
|
|
3
|
+
export declare function findExecutableOnPath(cmd: string): string | null;
|
|
4
|
+
export declare const sageCmd: SubcommandHandler;
|
|
5
|
+
//# sourceMappingURL=sage.d.ts.map
|