@wrongstack/core 0.9.1 → 0.9.7
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/dist/{agent-subagent-runner-R0I0Xusn.d.ts → agent-subagent-runner-DaF_EgRG.d.ts} +1 -1
- package/dist/{config-DoUAEvXH.d.ts → config-SkMIDN9L.d.ts} +15 -1
- package/dist/coordination/index.d.ts +6 -6
- package/dist/defaults/index.d.ts +9 -9
- package/dist/execution/index.d.ts +6 -6
- package/dist/extension/index.d.ts +2 -2
- package/dist/{index-u2ROAV0F.d.ts → index-Bsha5K4D.d.ts} +1 -1
- package/dist/{index-bOl8lzFV.d.ts → index-CP8638Wm.d.ts} +1 -1
- package/dist/index.d.ts +129 -20
- package/dist/index.js +1474 -57
- package/dist/index.js.map +1 -1
- package/dist/infrastructure/index.d.ts +2 -2
- package/dist/kernel/index.d.ts +2 -2
- package/dist/{mcp-servers-jEoMIBZv.d.ts → mcp-servers-BouUWYW6.d.ts} +1 -1
- package/dist/{multi-agent-coordinator-COwCGIrU.d.ts → multi-agent-coordinator-DTXF2aAl.d.ts} +1 -1
- package/dist/{null-fleet-bus-C1grTnOF.d.ts → null-fleet-bus-Chrc_3Pp.d.ts} +2 -2
- package/dist/{plan-templates-ByT1HcRI.d.ts → plan-templates-envSmNlZ.d.ts} +2 -2
- package/dist/sdd/index.d.ts +3 -3
- package/dist/storage/index.d.ts +74 -5
- package/dist/storage/index.js +352 -5
- package/dist/storage/index.js.map +1 -1
- package/dist/{tool-executor-D7a00JVJ.d.ts → tool-executor-CsktM3h9.d.ts} +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +3 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/{wstack-paths-CJYrjQU8.d.ts → wstack-paths-BCgmTNlG.d.ts} +4 -0
- package/package.json +1 -1
- package/skills/api-design/SKILL.md +139 -0
- package/skills/audit-log/SKILL.md +87 -14
- package/skills/bug-hunter/SKILL.md +42 -19
- package/skills/docker-deploy/SKILL.md +155 -0
- package/skills/git-flow/SKILL.md +53 -1
- package/skills/multi-agent/SKILL.md +42 -0
- package/skills/node-modern/SKILL.md +57 -1
- package/skills/observability/SKILL.md +134 -0
- package/skills/prompt-engineering/SKILL.md +46 -19
- package/skills/react-modern/SKILL.md +92 -1
- package/skills/refactor-planner/SKILL.md +49 -1
- package/skills/sdd/SKILL.md +12 -1
- package/skills/security-scanner/SKILL.md +46 -1
- package/skills/skill-creator/SKILL.md +49 -52
- package/skills/testing/SKILL.md +170 -0
- package/skills/typescript-strict/SKILL.md +98 -1
package/dist/index.js
CHANGED
|
@@ -56,8 +56,8 @@ async function atomicWrite(targetPath, content, opts = {}) {
|
|
|
56
56
|
}
|
|
57
57
|
let mode;
|
|
58
58
|
try {
|
|
59
|
-
const
|
|
60
|
-
mode =
|
|
59
|
+
const stat9 = await fsp2.stat(targetPath);
|
|
60
|
+
mode = stat9.mode & 511;
|
|
61
61
|
} catch {
|
|
62
62
|
mode = opts.mode;
|
|
63
63
|
}
|
|
@@ -3916,6 +3916,7 @@ function resolveWstackPaths(opts) {
|
|
|
3916
3916
|
secretsKey: path6.join(globalRoot, ".key"),
|
|
3917
3917
|
globalMemory: path6.join(globalRoot, "memory.md"),
|
|
3918
3918
|
globalSkills: path6.join(globalRoot, "skills"),
|
|
3919
|
+
globalPrompts: path6.join(globalRoot, "prompts"),
|
|
3919
3920
|
cacheDir: path6.join(globalRoot, "cache"),
|
|
3920
3921
|
modelsCache: path6.join(globalRoot, "cache", "models.dev.json"),
|
|
3921
3922
|
historyFile: path6.join(globalRoot, "history"),
|
|
@@ -3935,7 +3936,8 @@ function resolveWstackPaths(opts) {
|
|
|
3935
3936
|
projectTaskGraphs: path6.join(projectDir, "task-graphs"),
|
|
3936
3937
|
projectSddSession: path6.join(projectDir, "sdd-session.json"),
|
|
3937
3938
|
projectPlan: path6.join(projectDir, "plan.json"),
|
|
3938
|
-
projectAutophase: path6.join(projectDir, "autophase")
|
|
3939
|
+
projectAutophase: path6.join(projectDir, "autophase"),
|
|
3940
|
+
syncConfig: path6.join(globalRoot, "sync.json")
|
|
3939
3941
|
};
|
|
3940
3942
|
}
|
|
3941
3943
|
|
|
@@ -4034,11 +4036,11 @@ function validateAgainstSchema(value, schema) {
|
|
|
4034
4036
|
walk2(value, schema, "", errors);
|
|
4035
4037
|
return { ok: errors.length === 0, errors };
|
|
4036
4038
|
}
|
|
4037
|
-
function walk2(value, schema,
|
|
4039
|
+
function walk2(value, schema, path31, errors) {
|
|
4038
4040
|
if (schema.enum !== void 0) {
|
|
4039
4041
|
if (!schema.enum.some((e) => deepEqual(e, value))) {
|
|
4040
4042
|
errors.push({
|
|
4041
|
-
path:
|
|
4043
|
+
path: path31 || "<root>",
|
|
4042
4044
|
message: `expected one of ${JSON.stringify(schema.enum)}, got ${JSON.stringify(value)}`
|
|
4043
4045
|
});
|
|
4044
4046
|
return;
|
|
@@ -4047,7 +4049,7 @@ function walk2(value, schema, path28, errors) {
|
|
|
4047
4049
|
if (typeof schema.type === "string") {
|
|
4048
4050
|
if (!checkType(value, schema.type)) {
|
|
4049
4051
|
errors.push({
|
|
4050
|
-
path:
|
|
4052
|
+
path: path31 || "<root>",
|
|
4051
4053
|
message: `expected ${schema.type}, got ${describeType(value)}`
|
|
4052
4054
|
});
|
|
4053
4055
|
return;
|
|
@@ -4057,19 +4059,19 @@ function walk2(value, schema, path28, errors) {
|
|
|
4057
4059
|
const obj = value;
|
|
4058
4060
|
for (const req of schema.required ?? []) {
|
|
4059
4061
|
if (!(req in obj)) {
|
|
4060
|
-
errors.push({ path: joinPath(
|
|
4062
|
+
errors.push({ path: joinPath(path31, req), message: "required property missing" });
|
|
4061
4063
|
}
|
|
4062
4064
|
}
|
|
4063
4065
|
if (schema.properties) {
|
|
4064
4066
|
for (const [key, subSchema] of Object.entries(schema.properties)) {
|
|
4065
4067
|
if (key in obj) {
|
|
4066
|
-
walk2(obj[key], subSchema, joinPath(
|
|
4068
|
+
walk2(obj[key], subSchema, joinPath(path31, key), errors);
|
|
4067
4069
|
}
|
|
4068
4070
|
}
|
|
4069
4071
|
}
|
|
4070
4072
|
}
|
|
4071
4073
|
if (schema.type === "array" && Array.isArray(value) && schema.items) {
|
|
4072
|
-
value.forEach((item, i) => walk2(item, schema.items, `${
|
|
4074
|
+
value.forEach((item, i) => walk2(item, schema.items, `${path31}[${i}]`, errors));
|
|
4073
4075
|
}
|
|
4074
4076
|
}
|
|
4075
4077
|
function checkType(value, type) {
|
|
@@ -4248,8 +4250,8 @@ var DefaultSessionStore = class {
|
|
|
4248
4250
|
return JSON.parse(raw);
|
|
4249
4251
|
} catch {
|
|
4250
4252
|
const full = this.sessionPath(id, ".jsonl");
|
|
4251
|
-
const
|
|
4252
|
-
const summary = await this.summarize(id,
|
|
4253
|
+
const stat9 = await fsp2.stat(full);
|
|
4254
|
+
const summary = await this.summarize(id, stat9.mtime.toISOString());
|
|
4253
4255
|
await atomicWrite(manifest, JSON.stringify(summary), { mode: 384 }).catch((err) => {
|
|
4254
4256
|
console.warn(
|
|
4255
4257
|
`[session-store] Failed to write manifest for "${id}":`,
|
|
@@ -8365,8 +8367,8 @@ ${recentJournal}` : "No prior iterations.",
|
|
|
8365
8367
|
await saveGoal(this.goalPath, abandoned);
|
|
8366
8368
|
}
|
|
8367
8369
|
try {
|
|
8368
|
-
const { unlink:
|
|
8369
|
-
await
|
|
8370
|
+
const { unlink: unlink13 } = await import('fs/promises');
|
|
8371
|
+
await unlink13(this.goalPath);
|
|
8370
8372
|
} catch {
|
|
8371
8373
|
}
|
|
8372
8374
|
this.opts.onEternalStop?.();
|
|
@@ -14347,8 +14349,8 @@ async function loadProjectModes(modesDir) {
|
|
|
14347
14349
|
for (const entry of entries) {
|
|
14348
14350
|
if (!entry.endsWith(".md") && !entry.endsWith(".txt")) continue;
|
|
14349
14351
|
const filePath = path6.join(modesDir, entry);
|
|
14350
|
-
const
|
|
14351
|
-
if (!
|
|
14352
|
+
const stat9 = await fsp2.stat(filePath);
|
|
14353
|
+
if (!stat9.isFile()) continue;
|
|
14352
14354
|
const content = await fsp2.readFile(filePath, "utf8");
|
|
14353
14355
|
const id = path6.basename(entry, path6.extname(entry));
|
|
14354
14356
|
modes.push({
|
|
@@ -15622,9 +15624,9 @@ var AISpecBuilder = class {
|
|
|
15622
15624
|
if (!this.sessionPath) return;
|
|
15623
15625
|
try {
|
|
15624
15626
|
const fsp18 = await import('fs/promises');
|
|
15625
|
-
const
|
|
15627
|
+
const path31 = await import('path');
|
|
15626
15628
|
const { atomicWrite: atomicWrite2 } = await Promise.resolve().then(() => (init_atomic_write(), atomic_write_exports));
|
|
15627
|
-
await fsp18.mkdir(
|
|
15629
|
+
await fsp18.mkdir(path31.dirname(this.sessionPath), { recursive: true });
|
|
15628
15630
|
await atomicWrite2(this.sessionPath, JSON.stringify(this.session, null, 2));
|
|
15629
15631
|
} catch {
|
|
15630
15632
|
}
|
|
@@ -16334,15 +16336,15 @@ function computeCriticalPath(graph, topoOrder, blockedByMap) {
|
|
|
16334
16336
|
maxId = id;
|
|
16335
16337
|
}
|
|
16336
16338
|
}
|
|
16337
|
-
const
|
|
16339
|
+
const path31 = [];
|
|
16338
16340
|
let current = maxId;
|
|
16339
16341
|
const visited = /* @__PURE__ */ new Set();
|
|
16340
16342
|
while (current && !visited.has(current)) {
|
|
16341
16343
|
visited.add(current);
|
|
16342
|
-
|
|
16344
|
+
path31.unshift(current);
|
|
16343
16345
|
current = prev.get(current) ?? null;
|
|
16344
16346
|
}
|
|
16345
|
-
return
|
|
16347
|
+
return path31;
|
|
16346
16348
|
}
|
|
16347
16349
|
function computeParallelGroups(graph, blockedByMap) {
|
|
16348
16350
|
const groups = [];
|
|
@@ -17322,7 +17324,7 @@ async function startMetricsServer(opts) {
|
|
|
17322
17324
|
const tls = opts.tls;
|
|
17323
17325
|
const useHttps = !!(tls?.cert && tls?.key);
|
|
17324
17326
|
const host = opts.host ?? "127.0.0.1";
|
|
17325
|
-
const
|
|
17327
|
+
const path31 = opts.path ?? "/metrics";
|
|
17326
17328
|
const healthPath = opts.healthPath ?? "/healthz";
|
|
17327
17329
|
const healthRegistry = opts.healthRegistry;
|
|
17328
17330
|
const listener = (req, res) => {
|
|
@@ -17332,7 +17334,7 @@ async function startMetricsServer(opts) {
|
|
|
17332
17334
|
return;
|
|
17333
17335
|
}
|
|
17334
17336
|
const url = req.url.split("?")[0];
|
|
17335
|
-
if (url ===
|
|
17337
|
+
if (url === path31) {
|
|
17336
17338
|
let body;
|
|
17337
17339
|
try {
|
|
17338
17340
|
body = renderPrometheus(opts.sink.snapshot());
|
|
@@ -17396,7 +17398,7 @@ async function startMetricsServer(opts) {
|
|
|
17396
17398
|
const protocol = useHttps ? "https" : "http";
|
|
17397
17399
|
return {
|
|
17398
17400
|
port: boundPort,
|
|
17399
|
-
url: `${protocol}://${host}:${boundPort}${
|
|
17401
|
+
url: `${protocol}://${host}:${boundPort}${path31}`,
|
|
17400
17402
|
close: () => new Promise((resolve9, reject) => {
|
|
17401
17403
|
server.close((err) => err ? reject(err) : resolve9());
|
|
17402
17404
|
})
|
|
@@ -18243,10 +18245,10 @@ var SkillInstaller = class {
|
|
|
18243
18245
|
if (!resolved.startsWith(path6.resolve(destDir))) {
|
|
18244
18246
|
throw new Error(`Path traversal detected in skill file: ${file}`);
|
|
18245
18247
|
}
|
|
18246
|
-
const
|
|
18247
|
-
if (
|
|
18248
|
+
const stat9 = await fsp2.stat(srcPath);
|
|
18249
|
+
if (stat9.size > MAX_SKILL_FILE_SIZE) {
|
|
18248
18250
|
throw new Error(
|
|
18249
|
-
`Skill file "${file}" is too large (${(
|
|
18251
|
+
`Skill file "${file}" is too large (${(stat9.size / 1024).toFixed(1)}KB). Max: ${MAX_SKILL_FILE_SIZE / 1024}KB`
|
|
18250
18252
|
);
|
|
18251
18253
|
}
|
|
18252
18254
|
await fsp2.mkdir(path6.dirname(destPath), { recursive: true });
|
|
@@ -18654,6 +18656,356 @@ async function revertSnapshots(snapshots, projectRoot) {
|
|
|
18654
18656
|
}
|
|
18655
18657
|
return { revertedFiles, errors };
|
|
18656
18658
|
}
|
|
18659
|
+
|
|
18660
|
+
// src/storage/prompt-store.ts
|
|
18661
|
+
init_atomic_write();
|
|
18662
|
+
var DefaultPromptStore = class {
|
|
18663
|
+
dir;
|
|
18664
|
+
constructor(paths) {
|
|
18665
|
+
this.dir = paths.globalPrompts;
|
|
18666
|
+
}
|
|
18667
|
+
async list() {
|
|
18668
|
+
await ensureDir(this.dir);
|
|
18669
|
+
let entries = [];
|
|
18670
|
+
try {
|
|
18671
|
+
const files = await fsp2.readdir(this.dir);
|
|
18672
|
+
for (const file of files) {
|
|
18673
|
+
if (!file.endsWith(".json")) continue;
|
|
18674
|
+
try {
|
|
18675
|
+
const raw = JSON.parse(
|
|
18676
|
+
await fsp2.readFile(path6.join(this.dir, file), "utf8")
|
|
18677
|
+
);
|
|
18678
|
+
entries.push(raw.entry);
|
|
18679
|
+
} catch {
|
|
18680
|
+
}
|
|
18681
|
+
}
|
|
18682
|
+
} catch {
|
|
18683
|
+
}
|
|
18684
|
+
return entries.sort(
|
|
18685
|
+
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
18686
|
+
);
|
|
18687
|
+
}
|
|
18688
|
+
async get(id) {
|
|
18689
|
+
const file = path6.join(this.dir, `${id}.json`);
|
|
18690
|
+
try {
|
|
18691
|
+
const raw = JSON.parse(await fsp2.readFile(file, "utf8"));
|
|
18692
|
+
return raw.entry;
|
|
18693
|
+
} catch {
|
|
18694
|
+
return null;
|
|
18695
|
+
}
|
|
18696
|
+
}
|
|
18697
|
+
async save(entry) {
|
|
18698
|
+
await ensureDir(this.dir);
|
|
18699
|
+
const file = path6.join(this.dir, `${entry.id}.json`);
|
|
18700
|
+
const raw = { version: 1, entry };
|
|
18701
|
+
await atomicWrite(file, JSON.stringify(raw, null, 2));
|
|
18702
|
+
}
|
|
18703
|
+
async delete(id) {
|
|
18704
|
+
const file = path6.join(this.dir, `${id}.json`);
|
|
18705
|
+
try {
|
|
18706
|
+
await fsp2.unlink(file);
|
|
18707
|
+
return true;
|
|
18708
|
+
} catch {
|
|
18709
|
+
return false;
|
|
18710
|
+
}
|
|
18711
|
+
}
|
|
18712
|
+
async find(query) {
|
|
18713
|
+
const all = await this.list();
|
|
18714
|
+
const lower = query.toLowerCase();
|
|
18715
|
+
return all.filter(
|
|
18716
|
+
(e) => e.title.toLowerCase().includes(lower) || e.content.toLowerCase().includes(lower) || e.tags.some((t2) => t2.toLowerCase().includes(lower))
|
|
18717
|
+
);
|
|
18718
|
+
}
|
|
18719
|
+
/** Create a new entry and return it. Does NOT persist — call save() afterwards. */
|
|
18720
|
+
createNew(title, content, tags = []) {
|
|
18721
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18722
|
+
return {
|
|
18723
|
+
id: randomUUID().slice(0, 8),
|
|
18724
|
+
title,
|
|
18725
|
+
content,
|
|
18726
|
+
tags,
|
|
18727
|
+
createdAt: now,
|
|
18728
|
+
updatedAt: now
|
|
18729
|
+
};
|
|
18730
|
+
}
|
|
18731
|
+
};
|
|
18732
|
+
var ALL_SYNC_CATEGORIES = ["settings", "skills", "prompts", "memory", "history"];
|
|
18733
|
+
var CloudSync = class {
|
|
18734
|
+
constructor(paths, getConfig, setConfig) {
|
|
18735
|
+
this.paths = paths;
|
|
18736
|
+
this.getConfig = getConfig;
|
|
18737
|
+
this.setConfig = setConfig;
|
|
18738
|
+
this.statePath = path6.join(paths.globalRoot, "sync-state.json");
|
|
18739
|
+
}
|
|
18740
|
+
paths;
|
|
18741
|
+
getConfig;
|
|
18742
|
+
setConfig;
|
|
18743
|
+
statePath;
|
|
18744
|
+
state = null;
|
|
18745
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
18746
|
+
async status() {
|
|
18747
|
+
const cfg = this.getConfig();
|
|
18748
|
+
if (!cfg?.enabled) {
|
|
18749
|
+
return "CloudSync: disabled. Run `/sync enable` to activate.";
|
|
18750
|
+
}
|
|
18751
|
+
const last = this.state?.lastSyncedAt;
|
|
18752
|
+
const since = last ? timeAgo(last) : "never";
|
|
18753
|
+
return [
|
|
18754
|
+
`CloudSync: enabled`,
|
|
18755
|
+
` repo: ${cfg.repo}`,
|
|
18756
|
+
` categories: ${cfg.categories.join(", ")}`,
|
|
18757
|
+
` last sync: ${since}`
|
|
18758
|
+
].join("\n");
|
|
18759
|
+
}
|
|
18760
|
+
async enable(_repo, _categories) {
|
|
18761
|
+
return "Enable via /sync enable.";
|
|
18762
|
+
}
|
|
18763
|
+
async disable() {
|
|
18764
|
+
const cfg = this.getConfig();
|
|
18765
|
+
if (!cfg) return "CloudSync is not configured.";
|
|
18766
|
+
const next = { ...cfg, enabled: false };
|
|
18767
|
+
await this.setConfig(next);
|
|
18768
|
+
return "CloudSync disabled. Local data kept.";
|
|
18769
|
+
}
|
|
18770
|
+
async push(token) {
|
|
18771
|
+
const cfg = this.getConfig();
|
|
18772
|
+
if (!cfg?.enabled) return { ok: false, action: "push", categories: [], message: "Not enabled." };
|
|
18773
|
+
const parts = cfg.repo.split("/");
|
|
18774
|
+
const owner = parts[0];
|
|
18775
|
+
const repoName = parts[1];
|
|
18776
|
+
const branch = "main";
|
|
18777
|
+
const baseTreeSha = this.state?.sha;
|
|
18778
|
+
const { treeEntries, rev } = await this.buildLocalTree(cfg.categories);
|
|
18779
|
+
const newTreeSha = await this.createGitTree(token, owner, repoName, treeEntries, baseTreeSha);
|
|
18780
|
+
const commitSha = await this.createCommit(
|
|
18781
|
+
token,
|
|
18782
|
+
owner,
|
|
18783
|
+
repoName,
|
|
18784
|
+
newTreeSha,
|
|
18785
|
+
baseTreeSha,
|
|
18786
|
+
`Sync ${cfg.categories.join(", ")} \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`
|
|
18787
|
+
);
|
|
18788
|
+
await this.updateRef(token, owner, repoName, branch, commitSha);
|
|
18789
|
+
const syncState = {
|
|
18790
|
+
version: 1,
|
|
18791
|
+
sha: commitSha,
|
|
18792
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18793
|
+
localRev: rev
|
|
18794
|
+
};
|
|
18795
|
+
await fsp2.writeFile(this.statePath, JSON.stringify(syncState, null, 2), "utf8");
|
|
18796
|
+
this.state = syncState;
|
|
18797
|
+
return {
|
|
18798
|
+
ok: true,
|
|
18799
|
+
action: "push",
|
|
18800
|
+
categories: cfg.categories,
|
|
18801
|
+
committedAt: commitSha,
|
|
18802
|
+
message: `Pushed ${cfg.categories.join(", ")} to ${cfg.repo}. Commit: ${commitSha.slice(0, 7)}`
|
|
18803
|
+
};
|
|
18804
|
+
}
|
|
18805
|
+
async pull(token) {
|
|
18806
|
+
const cfg = this.getConfig();
|
|
18807
|
+
if (!cfg?.enabled) return { ok: false, action: "pull", categories: [], message: "Not enabled." };
|
|
18808
|
+
const pullParts = cfg.repo.split("/");
|
|
18809
|
+
const owner = pullParts[0];
|
|
18810
|
+
const repoName = pullParts[1];
|
|
18811
|
+
const branchData = await this.getRef(token, owner, repoName, "main");
|
|
18812
|
+
const currentSha = branchData.object.sha;
|
|
18813
|
+
const commitData = await this.getCommit(token, owner, repoName, currentSha);
|
|
18814
|
+
const treeSha = commitData.tree.sha;
|
|
18815
|
+
const treeEntries = await this.getTreeEntries(token, owner, repoName, treeSha);
|
|
18816
|
+
for (const entry of treeEntries) {
|
|
18817
|
+
if (entry.type !== "blob") continue;
|
|
18818
|
+
const segments = entry.path.split("/");
|
|
18819
|
+
if (segments[0] !== "data" || !segments[1]) continue;
|
|
18820
|
+
const cat = segments[1];
|
|
18821
|
+
if (!["settings", "skills", "prompts", "memory", "history"].includes(cat)) continue;
|
|
18822
|
+
const localPath = this.categoryToPath(cat);
|
|
18823
|
+
if (!localPath) continue;
|
|
18824
|
+
const rel = segments.slice(2).join("/");
|
|
18825
|
+
const destPath = rel ? path6.join(localPath, rel) : localPath;
|
|
18826
|
+
const blobData = await this.getBlob(token, owner, repoName, entry.sha);
|
|
18827
|
+
await fsp2.mkdir(path6.dirname(destPath), { recursive: true });
|
|
18828
|
+
await fsp2.writeFile(destPath, Buffer.from(blobData, "base64"));
|
|
18829
|
+
}
|
|
18830
|
+
const localRev = await this.hashLocalCategories(cfg.categories);
|
|
18831
|
+
const syncState = {
|
|
18832
|
+
version: 1,
|
|
18833
|
+
sha: currentSha,
|
|
18834
|
+
lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
18835
|
+
localRev
|
|
18836
|
+
};
|
|
18837
|
+
await fsp2.writeFile(this.statePath, JSON.stringify(syncState, null, 2), "utf8");
|
|
18838
|
+
this.state = syncState;
|
|
18839
|
+
return {
|
|
18840
|
+
ok: true,
|
|
18841
|
+
action: "pull",
|
|
18842
|
+
categories: cfg.categories,
|
|
18843
|
+
committedAt: currentSha,
|
|
18844
|
+
message: `Pulled ${cfg.categories.join(", ")} from ${cfg.repo}. Commit: ${currentSha.slice(0, 7)}`
|
|
18845
|
+
};
|
|
18846
|
+
}
|
|
18847
|
+
async hasLocalChanges() {
|
|
18848
|
+
if (!this.state) return true;
|
|
18849
|
+
const cfg = this.getConfig();
|
|
18850
|
+
if (!cfg) return true;
|
|
18851
|
+
const current = await this.hashLocalCategories(cfg.categories);
|
|
18852
|
+
return current !== this.state.localRev;
|
|
18853
|
+
}
|
|
18854
|
+
async loadState() {
|
|
18855
|
+
try {
|
|
18856
|
+
const raw = await fsp2.readFile(this.statePath, "utf8");
|
|
18857
|
+
this.state = JSON.parse(raw);
|
|
18858
|
+
} catch {
|
|
18859
|
+
this.state = null;
|
|
18860
|
+
}
|
|
18861
|
+
}
|
|
18862
|
+
// ── GitHub API helpers ──────────────────────────────────────────────
|
|
18863
|
+
async githubFetch(token, owner, repo, method, pathSegment, body) {
|
|
18864
|
+
const url = `https://api.github.com/repos/${owner}/${repo}${pathSegment}`;
|
|
18865
|
+
const res = await fetch(url, {
|
|
18866
|
+
method,
|
|
18867
|
+
headers: {
|
|
18868
|
+
Authorization: `Bearer ${token}`,
|
|
18869
|
+
Accept: "application/vnd.github+json",
|
|
18870
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
18871
|
+
"Content-Type": "application/json"
|
|
18872
|
+
},
|
|
18873
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
18874
|
+
});
|
|
18875
|
+
if (!res.ok) {
|
|
18876
|
+
const errText = await res.text();
|
|
18877
|
+
throw new Error(`GitHub API ${method} ${pathSegment} failed (${res.status}): ${errText}`);
|
|
18878
|
+
}
|
|
18879
|
+
const text = await res.text();
|
|
18880
|
+
return text ? JSON.parse(text) : {};
|
|
18881
|
+
}
|
|
18882
|
+
async getRef(token, owner, repo, ref) {
|
|
18883
|
+
return await this.githubFetch(token, owner, repo, "GET", `/git/refs/heads/${ref}`);
|
|
18884
|
+
}
|
|
18885
|
+
async updateRef(token, owner, repo, ref, sha) {
|
|
18886
|
+
await this.githubFetch(token, owner, repo, "PATCH", `/git/refs/heads/${ref}`, {
|
|
18887
|
+
sha,
|
|
18888
|
+
force: false
|
|
18889
|
+
});
|
|
18890
|
+
}
|
|
18891
|
+
async getCommit(token, owner, repo, sha) {
|
|
18892
|
+
return await this.githubFetch(token, owner, repo, "GET", `/git/commits/${sha}`);
|
|
18893
|
+
}
|
|
18894
|
+
async getTreeEntries(token, owner, repo, treeSha) {
|
|
18895
|
+
return await this.githubFetch(token, owner, repo, "GET", `/git/trees/${treeSha}?recursive=1`);
|
|
18896
|
+
}
|
|
18897
|
+
async createCommit(token, owner, repo, treeSha, parentSha, message = "sync") {
|
|
18898
|
+
const body = { message, tree: treeSha };
|
|
18899
|
+
if (parentSha) body.parents = [parentSha];
|
|
18900
|
+
const result = await this.githubFetch(token, owner, repo, "POST", "/git/commits", body);
|
|
18901
|
+
return result.sha;
|
|
18902
|
+
}
|
|
18903
|
+
async createGitTree(token, owner, repo, entries, baseTreeSha) {
|
|
18904
|
+
const tree = entries.map((e) => ({
|
|
18905
|
+
path: e.path,
|
|
18906
|
+
mode: e.mode,
|
|
18907
|
+
type: "blob",
|
|
18908
|
+
content: e.content
|
|
18909
|
+
}));
|
|
18910
|
+
const body = { tree };
|
|
18911
|
+
if (baseTreeSha) body.base_tree = baseTreeSha;
|
|
18912
|
+
const result = await this.githubFetch(token, owner, repo, "POST", "/git/trees", body);
|
|
18913
|
+
return result.sha;
|
|
18914
|
+
}
|
|
18915
|
+
async getBlob(token, owner, repo, sha) {
|
|
18916
|
+
const result = await this.githubFetch(token, owner, repo, "GET", `/git/blobs/${sha}`);
|
|
18917
|
+
return result.content;
|
|
18918
|
+
}
|
|
18919
|
+
// ── Local file helpers ──────────────────────────────────────────────
|
|
18920
|
+
async buildLocalTree(categories) {
|
|
18921
|
+
const entries = [];
|
|
18922
|
+
const hashes = [];
|
|
18923
|
+
for (const cat of categories) {
|
|
18924
|
+
const localPath = this.categoryToPath(cat);
|
|
18925
|
+
if (!localPath) continue;
|
|
18926
|
+
try {
|
|
18927
|
+
const stat9 = await fsp2.stat(localPath);
|
|
18928
|
+
if (stat9.isDirectory()) {
|
|
18929
|
+
const files = await this.walkDir(localPath, localPath);
|
|
18930
|
+
for (const file of files) {
|
|
18931
|
+
const content = await fsp2.readFile(file, "utf8");
|
|
18932
|
+
const rel = path6.relative(localPath, file).replace(/\\/g, "/");
|
|
18933
|
+
entries.push({ path: `data/${cat}/${rel}`, content, mode: "100644" });
|
|
18934
|
+
hashes.push(content);
|
|
18935
|
+
}
|
|
18936
|
+
} else {
|
|
18937
|
+
const content = await fsp2.readFile(localPath, "utf8");
|
|
18938
|
+
entries.push({ path: `data/${cat}`, content, mode: "100644" });
|
|
18939
|
+
hashes.push(content);
|
|
18940
|
+
}
|
|
18941
|
+
} catch {
|
|
18942
|
+
}
|
|
18943
|
+
}
|
|
18944
|
+
const rev = createHash("sha256").update(hashes.join("")).digest("hex").slice(0, 12);
|
|
18945
|
+
return { treeEntries: entries, rev };
|
|
18946
|
+
}
|
|
18947
|
+
async hashLocalCategories(categories) {
|
|
18948
|
+
const hashes = [];
|
|
18949
|
+
for (const cat of categories) {
|
|
18950
|
+
const localPath = this.categoryToPath(cat);
|
|
18951
|
+
if (!localPath) continue;
|
|
18952
|
+
try {
|
|
18953
|
+
const stat9 = await fsp2.stat(localPath);
|
|
18954
|
+
if (stat9.isDirectory()) {
|
|
18955
|
+
const files = await this.walkDir(localPath, localPath);
|
|
18956
|
+
for (const file of files) {
|
|
18957
|
+
const content = await fsp2.readFile(file);
|
|
18958
|
+
hashes.push(content.toString("base64") + file);
|
|
18959
|
+
}
|
|
18960
|
+
} else {
|
|
18961
|
+
const content = await fsp2.readFile(localPath);
|
|
18962
|
+
hashes.push(content.toString("base64") + localPath);
|
|
18963
|
+
}
|
|
18964
|
+
} catch {
|
|
18965
|
+
}
|
|
18966
|
+
}
|
|
18967
|
+
return createHash("sha256").update(hashes.join("")).digest("hex").slice(0, 12);
|
|
18968
|
+
}
|
|
18969
|
+
categoryToPath(cat) {
|
|
18970
|
+
switch (cat) {
|
|
18971
|
+
case "settings":
|
|
18972
|
+
return this.paths.globalConfig;
|
|
18973
|
+
case "skills":
|
|
18974
|
+
return this.paths.globalSkills;
|
|
18975
|
+
case "prompts":
|
|
18976
|
+
return this.paths.globalPrompts;
|
|
18977
|
+
case "memory":
|
|
18978
|
+
return this.paths.globalMemory;
|
|
18979
|
+
case "history":
|
|
18980
|
+
return this.paths.historyFile;
|
|
18981
|
+
default:
|
|
18982
|
+
return null;
|
|
18983
|
+
}
|
|
18984
|
+
}
|
|
18985
|
+
async walkDir(dir, base) {
|
|
18986
|
+
const results = [];
|
|
18987
|
+
const entries = await fsp2.readdir(dir, { withFileTypes: true });
|
|
18988
|
+
for (const entry of entries) {
|
|
18989
|
+
const full = path6.join(dir, entry.name);
|
|
18990
|
+
if (entry.isDirectory()) {
|
|
18991
|
+
results.push(...await this.walkDir(full, base));
|
|
18992
|
+
} else {
|
|
18993
|
+
results.push(full);
|
|
18994
|
+
}
|
|
18995
|
+
}
|
|
18996
|
+
return results;
|
|
18997
|
+
}
|
|
18998
|
+
};
|
|
18999
|
+
function timeAgo(iso) {
|
|
19000
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
19001
|
+
const mins = Math.floor(diff / 6e4);
|
|
19002
|
+
if (mins < 1) return "just now";
|
|
19003
|
+
if (mins < 60) return `${mins}m ago`;
|
|
19004
|
+
const hrs = Math.floor(mins / 60);
|
|
19005
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
19006
|
+
const days = Math.floor(hrs / 24);
|
|
19007
|
+
return `${days}d ago`;
|
|
19008
|
+
}
|
|
18657
19009
|
var MATCHERS = {
|
|
18658
19010
|
pnpmWorkspace: (files) => files.includes("pnpm-workspace.yaml"),
|
|
18659
19011
|
gradlew: (_files, dirs) => dirs.includes("gradlew"),
|
|
@@ -19531,8 +19883,8 @@ var ReportGenerator = class {
|
|
|
19531
19883
|
try {
|
|
19532
19884
|
await stat(this.options.outputDir);
|
|
19533
19885
|
} catch {
|
|
19534
|
-
const { mkdir:
|
|
19535
|
-
await
|
|
19886
|
+
const { mkdir: mkdir15 } = await import('fs/promises');
|
|
19887
|
+
await mkdir15(this.options.outputDir, { recursive: true });
|
|
19536
19888
|
}
|
|
19537
19889
|
}
|
|
19538
19890
|
generateMarkdown(result) {
|
|
@@ -20454,16 +20806,16 @@ Use \`/security report <number>\` to view a specific report.` };
|
|
|
20454
20806
|
}
|
|
20455
20807
|
const index = Number.parseInt(reportId, 10) - 1;
|
|
20456
20808
|
if (!Number.isNaN(index) && reports[index]) {
|
|
20457
|
-
const { readFile:
|
|
20458
|
-
const content = await
|
|
20809
|
+
const { readFile: readFile34 } = await import('fs/promises');
|
|
20810
|
+
const content = await readFile34(join(reportsDir, reports[index]), "utf-8");
|
|
20459
20811
|
return { message: `# Security Report
|
|
20460
20812
|
|
|
20461
20813
|
${content}` };
|
|
20462
20814
|
}
|
|
20463
20815
|
const match = reports.find((r) => r.includes(reportId));
|
|
20464
20816
|
if (match) {
|
|
20465
|
-
const { readFile:
|
|
20466
|
-
const content = await
|
|
20817
|
+
const { readFile: readFile34 } = await import('fs/promises');
|
|
20818
|
+
const content = await readFile34(join(reportsDir, match), "utf-8");
|
|
20467
20819
|
return { message: `# Security Report
|
|
20468
20820
|
|
|
20469
20821
|
${content}` };
|
|
@@ -22625,8 +22977,8 @@ ${mem}`);
|
|
|
22625
22977
|
}
|
|
22626
22978
|
async dirExists(p) {
|
|
22627
22979
|
try {
|
|
22628
|
-
const
|
|
22629
|
-
return
|
|
22980
|
+
const stat9 = await fsp2.stat(p);
|
|
22981
|
+
return stat9.isDirectory();
|
|
22630
22982
|
} catch {
|
|
22631
22983
|
return false;
|
|
22632
22984
|
}
|
|
@@ -22887,39 +23239,55 @@ var ProviderRegistry = class {
|
|
|
22887
23239
|
var SlashCommandRegistry = class {
|
|
22888
23240
|
cmds = /* @__PURE__ */ new Map();
|
|
22889
23241
|
/**
|
|
22890
|
-
* Register a command.
|
|
22891
|
-
*
|
|
22892
|
-
*
|
|
23242
|
+
* Register a command.
|
|
23243
|
+
*
|
|
23244
|
+
* Trust tiers, by `owner` and `opts.official`:
|
|
23245
|
+
*
|
|
23246
|
+
* - **Built-ins** (`owner === 'core'`) and **official plugins**
|
|
23247
|
+
* (`opts.official === true`, set by the host only for first-party plugins
|
|
23248
|
+
* loaded from the built-in factory list) claim the **bare** command name
|
|
23249
|
+
* (`/prompts`). They may override one another — last write wins — so an
|
|
23250
|
+
* official plugin can replace a built-in. Official plugins are *also*
|
|
23251
|
+
* reachable under their `owner:name` namespace.
|
|
23252
|
+
* - **External plugins** (any other `owner`) are isolated under the
|
|
23253
|
+
* `owner:name` namespace: invocable only as `/owner:cmd`, never by bare
|
|
23254
|
+
* name, and unable to shadow or override a built-in or official command.
|
|
23255
|
+
*
|
|
23256
|
+
* Officiality is supplied by the host based on the plugin's load source, not
|
|
23257
|
+
* self-declared by the plugin — an external plugin cannot name itself into
|
|
23258
|
+
* the official tier.
|
|
22893
23259
|
*/
|
|
22894
|
-
register(cmd, owner = "core") {
|
|
23260
|
+
register(cmd, owner = "core", opts) {
|
|
22895
23261
|
const isPlugin = owner !== "core";
|
|
22896
|
-
const
|
|
22897
|
-
|
|
22898
|
-
|
|
22899
|
-
|
|
23262
|
+
const official = !isPlugin || opts?.official === true;
|
|
23263
|
+
if (official) {
|
|
23264
|
+
const existing = this.cmds.get(cmd.name);
|
|
23265
|
+
if (existing) {
|
|
23266
|
+
const existingIsOfficialPlugin = existing.official && existing.owner !== "core";
|
|
23267
|
+
if (!isPlugin && existingIsOfficialPlugin) {
|
|
23268
|
+
return;
|
|
23269
|
+
}
|
|
23270
|
+
if (!isPlugin && existing.owner === "core") return;
|
|
22900
23271
|
}
|
|
22901
|
-
|
|
22902
|
-
if (this.cmds.has(fullName)) {
|
|
22903
|
-
if (!isPlugin) return;
|
|
22904
|
-
this.cmds.set(fullName, { cmd, owner });
|
|
23272
|
+
this.cmds.set(cmd.name, { cmd, owner, official });
|
|
22905
23273
|
for (const a of cmd.aliases ?? []) {
|
|
22906
|
-
this.cmds.set(
|
|
23274
|
+
this.cmds.set(a, { cmd, owner, official });
|
|
22907
23275
|
}
|
|
22908
|
-
return;
|
|
22909
23276
|
}
|
|
22910
|
-
|
|
22911
|
-
|
|
22912
|
-
|
|
23277
|
+
if (isPlugin) {
|
|
23278
|
+
this.cmds.set(`${owner}:${cmd.name}`, { cmd, owner, official });
|
|
23279
|
+
for (const a of cmd.aliases ?? []) {
|
|
23280
|
+
this.cmds.set(`${owner}:${a}`, { cmd, owner, official });
|
|
23281
|
+
}
|
|
22913
23282
|
}
|
|
22914
23283
|
}
|
|
22915
23284
|
unregister(name) {
|
|
22916
23285
|
const entry = this.cmds.get(name);
|
|
22917
23286
|
if (!entry) return false;
|
|
22918
|
-
for (const
|
|
22919
|
-
|
|
22920
|
-
this.cmds.delete(fullAlias);
|
|
23287
|
+
for (const [key, e] of this.cmds.entries()) {
|
|
23288
|
+
if (e.cmd === entry.cmd) this.cmds.delete(key);
|
|
22921
23289
|
}
|
|
22922
|
-
return
|
|
23290
|
+
return true;
|
|
22923
23291
|
}
|
|
22924
23292
|
/**
|
|
22925
23293
|
* Bulk-register multiple slash commands at once.
|
|
@@ -23038,8 +23406,9 @@ var DefaultPluginAPI = class {
|
|
|
23038
23406
|
};
|
|
23039
23407
|
this.mcp = init.mcpRegistry ?? noopMcp;
|
|
23040
23408
|
const scr = init.slashCommandRegistry;
|
|
23409
|
+
const official = init.official === true;
|
|
23041
23410
|
this.slashCommands = scr ? {
|
|
23042
|
-
register: (cmd) => scr.register(cmd, owner),
|
|
23411
|
+
register: (cmd) => scr.register(cmd, owner, { official }),
|
|
23043
23412
|
unregister: (name) => scr.unregister(name),
|
|
23044
23413
|
get: (name) => scr.get(name),
|
|
23045
23414
|
list: () => scr.list()
|
|
@@ -24927,6 +25296,1054 @@ function assertSafePath(dir, projectRoot) {
|
|
|
24927
25296
|
}
|
|
24928
25297
|
}
|
|
24929
25298
|
|
|
24930
|
-
|
|
25299
|
+
// src/plugins/prompts-plugin.ts
|
|
25300
|
+
function createPromptsPlugin(opts) {
|
|
25301
|
+
let store = null;
|
|
25302
|
+
return {
|
|
25303
|
+
name: "wstack-prompts",
|
|
25304
|
+
version: "1.0.0",
|
|
25305
|
+
description: "Personal prompt library with LLM-powered enhancements",
|
|
25306
|
+
apiVersion: "^0.1",
|
|
25307
|
+
capabilities: { slashCommands: true },
|
|
25308
|
+
defaultConfig: {},
|
|
25309
|
+
setup(api) {
|
|
25310
|
+
const rawConfig = api.config;
|
|
25311
|
+
const paths = opts?.paths ?? rawConfig.paths;
|
|
25312
|
+
store = opts?.store ?? (paths ? new DefaultPromptStore(paths) : null);
|
|
25313
|
+
api.slashCommands.register(buildPromptsCommand(store));
|
|
25314
|
+
api.log.info("[prompts] loaded \u2014 /prompts available");
|
|
25315
|
+
},
|
|
25316
|
+
teardown(api) {
|
|
25317
|
+
api.slashCommands.unregister("prompts");
|
|
25318
|
+
api.log.info("[prompts] unloaded");
|
|
25319
|
+
},
|
|
25320
|
+
async health() {
|
|
25321
|
+
return { ok: true, message: "Prompt store accessible" };
|
|
25322
|
+
}
|
|
25323
|
+
};
|
|
25324
|
+
}
|
|
25325
|
+
function buildPromptsCommand(store) {
|
|
25326
|
+
return {
|
|
25327
|
+
name: "prompts",
|
|
25328
|
+
description: "Manage your prompt library: /prompts [list|view|add|delete|edit|extend]",
|
|
25329
|
+
async run(args, ctx) {
|
|
25330
|
+
if (!store) return { message: "Prompt store not available." };
|
|
25331
|
+
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
25332
|
+
const restJoined = rest.join(" ");
|
|
25333
|
+
switch (verb) {
|
|
25334
|
+
case "":
|
|
25335
|
+
case "list":
|
|
25336
|
+
case "ls": {
|
|
25337
|
+
const entries = await store.list();
|
|
25338
|
+
if (entries.length === 0) {
|
|
25339
|
+
return { message: 'Prompt library empty. Add: /prompts add "title" "content"' };
|
|
25340
|
+
}
|
|
25341
|
+
const lines = entries.map(
|
|
25342
|
+
(e) => ` ${e.title} ${dim2(e.id)} ${e.tags.join(", ") || ""}`
|
|
25343
|
+
);
|
|
25344
|
+
return { message: `Prompt library (${entries.length}):
|
|
25345
|
+
${lines.join("\n")}
|
|
25346
|
+
` };
|
|
25347
|
+
}
|
|
25348
|
+
case "view":
|
|
25349
|
+
case "show": {
|
|
25350
|
+
if (!restJoined) return { message: "Usage: /prompts view <title>" };
|
|
25351
|
+
const matches = await store.find(restJoined);
|
|
25352
|
+
if (matches.length === 0) return { message: `No prompt matching "${restJoined}".` };
|
|
25353
|
+
const entry = matches.find((m) => m.title.toLowerCase() === restJoined.toLowerCase()) ?? matches[0];
|
|
25354
|
+
const tags = entry.tags.length > 0 ? ` [${entry.tags.join(", ")}]` : "";
|
|
25355
|
+
return {
|
|
25356
|
+
message: `# ${entry.title}${tags}
|
|
25357
|
+
|
|
25358
|
+
${entry.content}
|
|
25359
|
+
|
|
25360
|
+
${dim2(`id: ${entry.id} | created: ${entry.createdAt}`)}`
|
|
25361
|
+
};
|
|
25362
|
+
}
|
|
25363
|
+
case "add":
|
|
25364
|
+
case "new": {
|
|
25365
|
+
const parsed = parseTitleContent(restJoined);
|
|
25366
|
+
if (!parsed.title) return { message: 'Usage: /prompts add "title" "prompt content"' };
|
|
25367
|
+
const entry = store.createNew(parsed.title, parsed.content);
|
|
25368
|
+
await store.save(entry);
|
|
25369
|
+
return { message: `Added prompt "${entry.title}" (${entry.id}).` };
|
|
25370
|
+
}
|
|
25371
|
+
case "delete":
|
|
25372
|
+
case "rm": {
|
|
25373
|
+
if (!restJoined) return { message: "Usage: /prompts delete <title>" };
|
|
25374
|
+
const matches = await store.find(restJoined);
|
|
25375
|
+
if (matches.length === 0) return { message: `No prompt matching "${restJoined}".` };
|
|
25376
|
+
const exact = matches.find((m) => m.title.toLowerCase() === restJoined.toLowerCase()) ?? matches[0];
|
|
25377
|
+
const deleted = await store.delete(exact.id);
|
|
25378
|
+
return { message: deleted ? `Deleted "${exact.title}".` : "Delete failed." };
|
|
25379
|
+
}
|
|
25380
|
+
case "edit":
|
|
25381
|
+
case "update": {
|
|
25382
|
+
const parsed = parseTitleContent(restJoined);
|
|
25383
|
+
if (!parsed.title) return { message: 'Usage: /prompts edit "title" "new content"' };
|
|
25384
|
+
const matches = await store.find(parsed.title);
|
|
25385
|
+
if (matches.length === 0) return { message: `No prompt matching "${parsed.title}".` };
|
|
25386
|
+
const exact = matches.find((m) => m.title.toLowerCase() === parsed.title.toLowerCase()) ?? matches[0];
|
|
25387
|
+
exact.content = parsed.content;
|
|
25388
|
+
exact.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25389
|
+
await store.save(exact);
|
|
25390
|
+
return { message: `Updated "${exact.title}".` };
|
|
25391
|
+
}
|
|
25392
|
+
case "extend": {
|
|
25393
|
+
if (!restJoined) return { message: 'Usage: /prompts extend "title" <instructions>' };
|
|
25394
|
+
const parsed = parseTitleContent(restJoined);
|
|
25395
|
+
if (!parsed.title) return { message: 'Usage: /prompts extend "title" <instructions>' };
|
|
25396
|
+
const matches = await store.find(parsed.title);
|
|
25397
|
+
if (matches.length === 0) return { message: `No prompt matching "${parsed.title}".` };
|
|
25398
|
+
const exact = matches.find((m) => m.title.toLowerCase() === parsed.title.toLowerCase()) ?? matches[0];
|
|
25399
|
+
const prov = ctx.provider;
|
|
25400
|
+
if (!prov?.complete) return { message: "LLM not available. Configure a provider first." };
|
|
25401
|
+
const enhanced = await prov.complete(ctx.model, [
|
|
25402
|
+
"[SYSTEM INSTRUCTIONS]",
|
|
25403
|
+
"You are a prompt engineering assistant. Improve the following prompt by integrating the additional instructions seamlessly. Keep the same tone and format. Return only the improved prompt.",
|
|
25404
|
+
"",
|
|
25405
|
+
`EXISTING PROMPT:
|
|
25406
|
+
${exact.content}`,
|
|
25407
|
+
"",
|
|
25408
|
+
`ADDITIONAL INSTRUCTIONS:
|
|
25409
|
+
${parsed.content}`,
|
|
25410
|
+
"",
|
|
25411
|
+
"Respond with ONLY the improved prompt, no commentary."
|
|
25412
|
+
].join("\n"));
|
|
25413
|
+
exact.content = enhanced.trim();
|
|
25414
|
+
exact.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
25415
|
+
await store.save(exact);
|
|
25416
|
+
return { message: `Extended "${exact.title}".
|
|
25417
|
+
|
|
25418
|
+
${dim2("New content:")}
|
|
25419
|
+
${exact.content}` };
|
|
25420
|
+
}
|
|
25421
|
+
default:
|
|
25422
|
+
return {
|
|
25423
|
+
message: `Unknown subcommand "${verb}". Try: list | view | add | delete | edit | extend`
|
|
25424
|
+
};
|
|
25425
|
+
}
|
|
25426
|
+
}
|
|
25427
|
+
};
|
|
25428
|
+
}
|
|
25429
|
+
function parseTitleContent(args) {
|
|
25430
|
+
const trimmed = args.trim();
|
|
25431
|
+
if (!trimmed) return { title: "", content: "" };
|
|
25432
|
+
const doubleMatch = /^"([^"]+)"\s+"([^"]+)"$/.exec(trimmed) || /^'([^']+)'\s+'([^']+)'$/.exec(trimmed);
|
|
25433
|
+
if (doubleMatch) return { title: doubleMatch[1], content: doubleMatch[2] };
|
|
25434
|
+
const singleMatch = /^'([^']+)'\s+(.+)$/.exec(trimmed);
|
|
25435
|
+
if (singleMatch) return { title: singleMatch[1], content: singleMatch[2] };
|
|
25436
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
25437
|
+
if (firstSpace === -1) return { title: trimmed, content: "" };
|
|
25438
|
+
return { title: trimmed.slice(0, firstSpace), content: trimmed.slice(firstSpace + 1) };
|
|
25439
|
+
}
|
|
25440
|
+
function dim2(s) {
|
|
25441
|
+
return `\x1B[2m${s}\x1B[0m`;
|
|
25442
|
+
}
|
|
25443
|
+
|
|
25444
|
+
// src/plugins/sync-plugin.ts
|
|
25445
|
+
function createSyncPlugin(opts) {
|
|
25446
|
+
let cloud = null;
|
|
25447
|
+
let configStore;
|
|
25448
|
+
return {
|
|
25449
|
+
name: "wstack-sync",
|
|
25450
|
+
version: "1.0.0",
|
|
25451
|
+
description: "GitHub cloud sync for prompts, skills, settings, memory and history",
|
|
25452
|
+
apiVersion: "^0.1",
|
|
25453
|
+
capabilities: { slashCommands: true },
|
|
25454
|
+
defaultConfig: {},
|
|
25455
|
+
setup(api) {
|
|
25456
|
+
const rawConfig = api.config;
|
|
25457
|
+
const paths = opts?.paths ?? rawConfig.paths;
|
|
25458
|
+
configStore = opts?.configStore ?? rawConfig.configStore;
|
|
25459
|
+
if (!paths || !configStore) {
|
|
25460
|
+
api.log.warn("[sync] paths or configStore not available \u2014 /sync disabled");
|
|
25461
|
+
return;
|
|
25462
|
+
}
|
|
25463
|
+
cloud = new CloudSync(
|
|
25464
|
+
paths,
|
|
25465
|
+
() => {
|
|
25466
|
+
const cfg = configStore.get();
|
|
25467
|
+
return cfg.sync;
|
|
25468
|
+
},
|
|
25469
|
+
async (cfg) => {
|
|
25470
|
+
configStore.update({ sync: cfg });
|
|
25471
|
+
}
|
|
25472
|
+
);
|
|
25473
|
+
void cloud.loadState();
|
|
25474
|
+
api.slashCommands.register(buildSyncCommand(cloud, configStore));
|
|
25475
|
+
api.log.info("[sync] loaded \u2014 /sync available. Run /sync to get started.");
|
|
25476
|
+
},
|
|
25477
|
+
teardown(api) {
|
|
25478
|
+
api.slashCommands.unregister("sync");
|
|
25479
|
+
api.log.info("[sync] unloaded");
|
|
25480
|
+
},
|
|
25481
|
+
async health() {
|
|
25482
|
+
return { ok: true, message: "CloudSync ready" };
|
|
25483
|
+
}
|
|
25484
|
+
};
|
|
25485
|
+
}
|
|
25486
|
+
function buildSyncCommand(cloud, configStore) {
|
|
25487
|
+
return {
|
|
25488
|
+
name: "sync",
|
|
25489
|
+
description: "Cloud sync: /sync [status|enable|disable|push|pull|categories]",
|
|
25490
|
+
async run(args, _ctx) {
|
|
25491
|
+
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
25492
|
+
const restJoined = rest.join(" ");
|
|
25493
|
+
switch (verb) {
|
|
25494
|
+
case "":
|
|
25495
|
+
case "status": {
|
|
25496
|
+
const msg = await cloud.status();
|
|
25497
|
+
return { message: msg };
|
|
25498
|
+
}
|
|
25499
|
+
case "enable": {
|
|
25500
|
+
const parts = restJoined.trim().split(/\s+/);
|
|
25501
|
+
if (parts.length < 2) {
|
|
25502
|
+
return {
|
|
25503
|
+
message: [
|
|
25504
|
+
"Usage: /sync enable owner/repo TOKEN [cat1 cat2 ...]",
|
|
25505
|
+
" e.g. /sync enable myname/wrongstack-data ghp_xxx",
|
|
25506
|
+
` Categories (default: all): ${ALL_SYNC_CATEGORIES.join(", ")}`
|
|
25507
|
+
].join("\n")
|
|
25508
|
+
};
|
|
25509
|
+
}
|
|
25510
|
+
const [repo, token, ...cats] = parts;
|
|
25511
|
+
if (!repo || !repo.includes("/")) {
|
|
25512
|
+
return { message: 'Invalid repo format. Expected "owner/repo".' };
|
|
25513
|
+
}
|
|
25514
|
+
const syncConfig = {
|
|
25515
|
+
enabled: true,
|
|
25516
|
+
repo,
|
|
25517
|
+
githubToken: token,
|
|
25518
|
+
categories: cats.length > 0 ? cats : ALL_SYNC_CATEGORIES,
|
|
25519
|
+
lastSyncedAt: void 0
|
|
25520
|
+
};
|
|
25521
|
+
configStore.update({ sync: syncConfig });
|
|
25522
|
+
await cloud.loadState();
|
|
25523
|
+
return {
|
|
25524
|
+
message: [
|
|
25525
|
+
`CloudSync enabled for ${repo}`,
|
|
25526
|
+
` categories: ${syncConfig.categories.join(", ")}`,
|
|
25527
|
+
"",
|
|
25528
|
+
"Run `/sync push` to upload your data to GitHub."
|
|
25529
|
+
].join("\n")
|
|
25530
|
+
};
|
|
25531
|
+
}
|
|
25532
|
+
case "disable": {
|
|
25533
|
+
const msg = await cloud.disable();
|
|
25534
|
+
return { message: msg };
|
|
25535
|
+
}
|
|
25536
|
+
case "push": {
|
|
25537
|
+
const cfg = configStore.get().sync;
|
|
25538
|
+
if (!cfg?.enabled) return { message: "CloudSync not enabled. Run `/sync enable`." };
|
|
25539
|
+
if (!cfg?.githubToken) return { message: "No GitHub token found. Run `/sync enable owner/repo TOKEN`." };
|
|
25540
|
+
let result;
|
|
25541
|
+
try {
|
|
25542
|
+
result = await cloud.push(cfg.githubToken);
|
|
25543
|
+
} catch (err) {
|
|
25544
|
+
return { message: `Push failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
25545
|
+
}
|
|
25546
|
+
if (result.ok) {
|
|
25547
|
+
configStore.update({
|
|
25548
|
+
sync: { ...cfg, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
25549
|
+
});
|
|
25550
|
+
}
|
|
25551
|
+
return { message: result.message };
|
|
25552
|
+
}
|
|
25553
|
+
case "pull": {
|
|
25554
|
+
const cfg = configStore.get().sync;
|
|
25555
|
+
if (!cfg?.enabled) return { message: "CloudSync not enabled. Run `/sync enable`." };
|
|
25556
|
+
if (!cfg?.githubToken) return { message: "No GitHub token. Run `/sync enable owner/repo TOKEN`." };
|
|
25557
|
+
let result;
|
|
25558
|
+
try {
|
|
25559
|
+
result = await cloud.pull(cfg.githubToken);
|
|
25560
|
+
} catch (err) {
|
|
25561
|
+
return { message: `Pull failed: ${err instanceof Error ? err.message : String(err)}` };
|
|
25562
|
+
}
|
|
25563
|
+
if (result.ok) {
|
|
25564
|
+
configStore.update({
|
|
25565
|
+
sync: { ...cfg, lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
25566
|
+
});
|
|
25567
|
+
}
|
|
25568
|
+
return { message: result.message };
|
|
25569
|
+
}
|
|
25570
|
+
case "categories": {
|
|
25571
|
+
const [action, ...catRest] = restJoined.trim().split(/\s+/);
|
|
25572
|
+
const cfg = configStore.get().sync;
|
|
25573
|
+
if (!cfg?.enabled) return { message: "CloudSync not enabled. Run `/sync enable`." };
|
|
25574
|
+
if (action === "list" || !action) {
|
|
25575
|
+
return {
|
|
25576
|
+
message: [
|
|
25577
|
+
`Synced categories: ${cfg.categories.join(", ")}`,
|
|
25578
|
+
`Available: ${ALL_SYNC_CATEGORIES.join(", ")}`,
|
|
25579
|
+
"Update: /sync categories add <name> | remove <name>"
|
|
25580
|
+
].join("\n")
|
|
25581
|
+
};
|
|
25582
|
+
}
|
|
25583
|
+
if (action === "add") {
|
|
25584
|
+
const catName = catRest[0];
|
|
25585
|
+
if (!ALL_SYNC_CATEGORIES.includes(catName)) {
|
|
25586
|
+
return { message: `Unknown. Available: ${ALL_SYNC_CATEGORIES.join(", ")}` };
|
|
25587
|
+
}
|
|
25588
|
+
if (cfg.categories.includes(catName)) return { message: `"${catName}" already synced.` };
|
|
25589
|
+
configStore.update({
|
|
25590
|
+
sync: { ...cfg, categories: [...cfg.categories, catName] }
|
|
25591
|
+
});
|
|
25592
|
+
return { message: `Added "${catName}" to sync categories.` };
|
|
25593
|
+
}
|
|
25594
|
+
if (action === "remove") {
|
|
25595
|
+
const catName = catRest[0];
|
|
25596
|
+
if (!cfg.categories.includes(catName)) return { message: `"${catName}" not in sync categories.` };
|
|
25597
|
+
configStore.update({
|
|
25598
|
+
sync: { ...cfg, categories: cfg.categories.filter((c) => c !== catName) }
|
|
25599
|
+
});
|
|
25600
|
+
return { message: `Removed "${catName}" from sync categories.` };
|
|
25601
|
+
}
|
|
25602
|
+
return { message: "Usage: /sync categories list | add <name> | remove <name>" };
|
|
25603
|
+
}
|
|
25604
|
+
default:
|
|
25605
|
+
return {
|
|
25606
|
+
message: [
|
|
25607
|
+
"/sync \u2014 Cloud Sync",
|
|
25608
|
+
"",
|
|
25609
|
+
" /sync status Show current sync status",
|
|
25610
|
+
" /sync enable owner/repo TOKEN Enable for a repo (TOKEN = fine-grained PAT)",
|
|
25611
|
+
" /sync disable Disable sync (keeps local data)",
|
|
25612
|
+
" /sync push Upload selected categories",
|
|
25613
|
+
" /sync pull Download from repo",
|
|
25614
|
+
" /sync categories list|add|remove Manage synced categories",
|
|
25615
|
+
"",
|
|
25616
|
+
`Categories: ${ALL_SYNC_CATEGORIES.join(", ")}`
|
|
25617
|
+
].join("\n")
|
|
25618
|
+
};
|
|
25619
|
+
}
|
|
25620
|
+
}
|
|
25621
|
+
};
|
|
25622
|
+
}
|
|
25623
|
+
function createGitPlugin() {
|
|
25624
|
+
return {
|
|
25625
|
+
name: "wstack-git",
|
|
25626
|
+
version: "1.0.0",
|
|
25627
|
+
description: "Git helpers: /commit (LLM message), /gitcheck, /push",
|
|
25628
|
+
apiVersion: "^0.1",
|
|
25629
|
+
capabilities: { slashCommands: true },
|
|
25630
|
+
defaultConfig: {},
|
|
25631
|
+
setup(api) {
|
|
25632
|
+
api.slashCommands.register(buildCommitCommand());
|
|
25633
|
+
api.slashCommands.register(buildGitcheckCommand());
|
|
25634
|
+
api.slashCommands.register(buildPushCommand());
|
|
25635
|
+
api.log.info("[git] loaded \u2014 /commit, /gitcheck, /push available");
|
|
25636
|
+
},
|
|
25637
|
+
teardown(api) {
|
|
25638
|
+
api.slashCommands.unregister("commit");
|
|
25639
|
+
api.slashCommands.unregister("gitcheck");
|
|
25640
|
+
api.slashCommands.unregister("push");
|
|
25641
|
+
api.log.info("[git] unloaded");
|
|
25642
|
+
},
|
|
25643
|
+
async health() {
|
|
25644
|
+
return { ok: true, message: "git helpers ready" };
|
|
25645
|
+
}
|
|
25646
|
+
};
|
|
25647
|
+
}
|
|
25648
|
+
async function runGit(args, cwd) {
|
|
25649
|
+
try {
|
|
25650
|
+
return await new Promise((resolve9, reject) => {
|
|
25651
|
+
const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
|
|
25652
|
+
let stdout = "";
|
|
25653
|
+
let stderr = "";
|
|
25654
|
+
child.stdout?.on("data", (d) => {
|
|
25655
|
+
stdout += d;
|
|
25656
|
+
});
|
|
25657
|
+
child.stderr?.on("data", (d) => {
|
|
25658
|
+
stderr += d;
|
|
25659
|
+
});
|
|
25660
|
+
child.on("error", (err) => {
|
|
25661
|
+
reject(
|
|
25662
|
+
new WrongStackError({
|
|
25663
|
+
message: `Failed to run git: ${err.message}`,
|
|
25664
|
+
code: ERROR_CODES.TOOL_EXECUTION_FAILED,
|
|
25665
|
+
subsystem: "tool",
|
|
25666
|
+
context: { command: "git", args, cwd },
|
|
25667
|
+
cause: err
|
|
25668
|
+
})
|
|
25669
|
+
);
|
|
25670
|
+
});
|
|
25671
|
+
child.on("close", (code) => resolve9({ stdout, stderr, code: code ?? 0 }));
|
|
25672
|
+
});
|
|
25673
|
+
} catch (err) {
|
|
25674
|
+
if (err instanceof WrongStackError) throw err;
|
|
25675
|
+
throw new WrongStackError({
|
|
25676
|
+
message: err instanceof Error ? err.message : String(err),
|
|
25677
|
+
code: ERROR_CODES.TOOL_EXECUTION_FAILED,
|
|
25678
|
+
subsystem: "tool",
|
|
25679
|
+
context: { command: "git", args, cwd },
|
|
25680
|
+
cause: err
|
|
25681
|
+
});
|
|
25682
|
+
}
|
|
25683
|
+
}
|
|
25684
|
+
async function isGitRepo(cwd) {
|
|
25685
|
+
const result = await runGit(["rev-parse", "--git-dir"], cwd);
|
|
25686
|
+
return result.code === 0;
|
|
25687
|
+
}
|
|
25688
|
+
async function hasUncommittedChanges(cwd) {
|
|
25689
|
+
const result = await runGit(["status", "--porcelain"], cwd);
|
|
25690
|
+
return result.stdout.trim().length > 0;
|
|
25691
|
+
}
|
|
25692
|
+
function asLLMProvider(provider) {
|
|
25693
|
+
if (provider && typeof provider.complete === "function") {
|
|
25694
|
+
return provider;
|
|
25695
|
+
}
|
|
25696
|
+
return null;
|
|
25697
|
+
}
|
|
25698
|
+
async function generateCommitMessageWithLLM(diff, provider, model) {
|
|
25699
|
+
const systemPrompt = "You are a helpful assistant that generates concise, conventional-commit-formatted git commit messages. Analyze the provided diff and output ONLY the commit message (no explanation, no quotes). Format: <type>(<scope>): <short description> \u2014 <type> is one of: feat, fix, docs, style, refactor, test, chore, perf, ci, build, temp. If the diff contains multiple unrelated changes, pick the most important one. Keep the description under 72 characters. Example: feat(cli): add /commit LLM integration";
|
|
25700
|
+
try {
|
|
25701
|
+
const ac = new AbortController();
|
|
25702
|
+
const timeout = setTimeout(() => ac.abort(), 15e3);
|
|
25703
|
+
const resp = await provider.complete(
|
|
25704
|
+
{
|
|
25705
|
+
model,
|
|
25706
|
+
system: [{ type: "text", text: systemPrompt }],
|
|
25707
|
+
messages: [
|
|
25708
|
+
{ role: "user", content: [{ type: "text", text: `Here is the git diff:
|
|
25709
|
+
|
|
25710
|
+
${diff}` }] }
|
|
25711
|
+
],
|
|
25712
|
+
maxTokens: 80,
|
|
25713
|
+
temperature: 0.3
|
|
25714
|
+
},
|
|
25715
|
+
{ signal: ac.signal }
|
|
25716
|
+
);
|
|
25717
|
+
clearTimeout(timeout);
|
|
25718
|
+
const raw = resp.content;
|
|
25719
|
+
const text = Array.isArray(raw) ? raw[0]?.text ?? "" : typeof raw === "object" && raw !== null ? raw.text ?? "" : String(raw ?? "");
|
|
25720
|
+
const message = text.trim().split("\n")[0] ?? "";
|
|
25721
|
+
if (message.length > 0 && message.length < 200) return message;
|
|
25722
|
+
} catch {
|
|
25723
|
+
}
|
|
25724
|
+
return null;
|
|
25725
|
+
}
|
|
25726
|
+
function detectCommitType(stats) {
|
|
25727
|
+
const lines = stats.split("\n");
|
|
25728
|
+
const hasTestFiles = lines.some(
|
|
25729
|
+
(l) => l.includes("_test.") || l.includes(".test.") || l.includes(".spec.")
|
|
25730
|
+
);
|
|
25731
|
+
const hasDocs = lines.some(
|
|
25732
|
+
(l) => l.includes("README") || l.includes("CHANGELOG") || l.includes("docs/") || l.includes(".md")
|
|
25733
|
+
);
|
|
25734
|
+
const hasConfig = lines.some(
|
|
25735
|
+
(l) => l.includes("config") || l.includes("tsconfig") || l.includes(".json")
|
|
25736
|
+
);
|
|
25737
|
+
if (hasTestFiles) return "test";
|
|
25738
|
+
if (hasDocs) return "docs";
|
|
25739
|
+
if (hasConfig) return "chore";
|
|
25740
|
+
return "feat";
|
|
25741
|
+
}
|
|
25742
|
+
async function generateCommitMessageHeuristics(cwd) {
|
|
25743
|
+
const statsResult = await runGit(["diff", "--stat"], cwd);
|
|
25744
|
+
if (statsResult.code !== 0) return "chore: update";
|
|
25745
|
+
const nameResult = await runGit(["diff", "--name-only"], cwd);
|
|
25746
|
+
const files = nameResult.stdout.split("\n").filter(Boolean);
|
|
25747
|
+
const commitType = detectCommitType(statsResult.stdout);
|
|
25748
|
+
let scope = "";
|
|
25749
|
+
if (files.length > 0) {
|
|
25750
|
+
const primary = files[0].split("/")[0];
|
|
25751
|
+
if (primary && primary !== "packages" && primary !== "apps" && primary !== "node_modules") {
|
|
25752
|
+
scope = `(${primary})`;
|
|
25753
|
+
}
|
|
25754
|
+
}
|
|
25755
|
+
if (files.length === 0) return `${commitType}${scope}: update`;
|
|
25756
|
+
if (files.length <= 3) {
|
|
25757
|
+
const summary2 = files.map((f) => f.split("/").pop()).join(", ");
|
|
25758
|
+
return `${commitType}${scope}: ${summary2}`;
|
|
25759
|
+
}
|
|
25760
|
+
const summary = `${files.slice(0, 3).map((f) => f.split("/").pop()).join(", ")} and ${files.length - 3} more`;
|
|
25761
|
+
return `${commitType}${scope}: ${summary}`;
|
|
25762
|
+
}
|
|
25763
|
+
function buildCommitCommand() {
|
|
25764
|
+
return {
|
|
25765
|
+
name: "commit",
|
|
25766
|
+
description: "Stage all changes and commit with auto-generated message.",
|
|
25767
|
+
aliases: ["gc"],
|
|
25768
|
+
async run(args, ctx) {
|
|
25769
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
25770
|
+
if (!await isGitRepo(cwd)) return { message: "Not a git repository." };
|
|
25771
|
+
if (!await hasUncommittedChanges(cwd)) {
|
|
25772
|
+
return { message: "Nothing to commit (working tree clean)." };
|
|
25773
|
+
}
|
|
25774
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
25775
|
+
const noLlm = args.includes("--no-llm");
|
|
25776
|
+
let message = null;
|
|
25777
|
+
const provider = noLlm ? null : asLLMProvider(ctx?.provider);
|
|
25778
|
+
if (provider && ctx?.model) {
|
|
25779
|
+
const diff = (await runGit(["diff"], cwd)).stdout;
|
|
25780
|
+
message = await generateCommitMessageWithLLM(diff, provider, ctx.model);
|
|
25781
|
+
}
|
|
25782
|
+
if (!message) message = await generateCommitMessageHeuristics(cwd);
|
|
25783
|
+
if (dryRun) {
|
|
25784
|
+
return {
|
|
25785
|
+
message: `Would commit:
|
|
25786
|
+
|
|
25787
|
+
${color.green(message)}
|
|
25788
|
+
|
|
25789
|
+
${color.dim("(dry-run \u2014 no actual commit)")}`
|
|
25790
|
+
};
|
|
25791
|
+
}
|
|
25792
|
+
const stageResult = await runGit(["add", "."], cwd);
|
|
25793
|
+
if (stageResult.code !== 0) return { message: `Stage failed: ${stageResult.stderr}` };
|
|
25794
|
+
const commitResult = await runGit(["commit", "-m", message], cwd);
|
|
25795
|
+
if (commitResult.code !== 0) return { message: `Commit failed: ${commitResult.stderr}` };
|
|
25796
|
+
const hash = (await runGit(["rev-parse", "--short", "HEAD"], cwd)).stdout.trim();
|
|
25797
|
+
const hasRemote = (await runGit(["remote"], cwd)).stdout.trim().length > 0;
|
|
25798
|
+
const pushMsg = hasRemote ? `
|
|
25799
|
+
|
|
25800
|
+
${color.dim("Tip: Run /push to push to remote")}` : "";
|
|
25801
|
+
return {
|
|
25802
|
+
message: `${color.green("\u2713")} Committed: ${color.bold(message)}
|
|
25803
|
+
${color.dim(hash)}${pushMsg}`
|
|
25804
|
+
};
|
|
25805
|
+
}
|
|
25806
|
+
};
|
|
25807
|
+
}
|
|
25808
|
+
function buildGitcheckCommand() {
|
|
25809
|
+
return {
|
|
25810
|
+
name: "gitcheck",
|
|
25811
|
+
description: "Check for uncommitted changes (for system prompt integration).",
|
|
25812
|
+
aliases: ["gcstatus"],
|
|
25813
|
+
async run(_args, ctx) {
|
|
25814
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
25815
|
+
if (!await isGitRepo(cwd)) return { message: "" };
|
|
25816
|
+
if (!await hasUncommittedChanges(cwd)) return { message: "" };
|
|
25817
|
+
const statusResult = await runGit(["status", "--porcelain"], cwd);
|
|
25818
|
+
const count = statusResult.stdout.split("\n").filter(Boolean).length;
|
|
25819
|
+
if (count === 0) return { message: "" };
|
|
25820
|
+
return {
|
|
25821
|
+
message: `\u26A0 ${color.yellow(`${count} uncommitted change${count > 1 ? "s" : ""}`)} \u2014 consider /commit`
|
|
25822
|
+
};
|
|
25823
|
+
}
|
|
25824
|
+
};
|
|
25825
|
+
}
|
|
25826
|
+
function buildPushCommand() {
|
|
25827
|
+
return {
|
|
25828
|
+
name: "push",
|
|
25829
|
+
description: "Push to remote after commit.",
|
|
25830
|
+
async run(args, ctx) {
|
|
25831
|
+
const cwd = ctx?.cwd ?? process.cwd();
|
|
25832
|
+
if (!await isGitRepo(cwd)) return { message: "Not a git repository." };
|
|
25833
|
+
const dryRun = args.includes("--dry-run") || args.includes("-n");
|
|
25834
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
25835
|
+
const remotes = (await runGit(["remote"], cwd)).stdout.split("\n").filter(Boolean);
|
|
25836
|
+
if (remotes.length === 0) {
|
|
25837
|
+
return { message: "No remote configured. Add one with: git remote add origin <url>" };
|
|
25838
|
+
}
|
|
25839
|
+
if (dryRun) {
|
|
25840
|
+
return {
|
|
25841
|
+
message: `Would push to ${remotes.join(", ")}${force ? " (force)" : ""}
|
|
25842
|
+
${color.dim("(dry-run)")}`
|
|
25843
|
+
};
|
|
25844
|
+
}
|
|
25845
|
+
const branch = (await runGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd)).stdout.trim() || "main";
|
|
25846
|
+
const pushArgs = ["push"];
|
|
25847
|
+
if (force) pushArgs.push("--force");
|
|
25848
|
+
pushArgs.push(...remotes, branch);
|
|
25849
|
+
const pushResult = await runGit(pushArgs, cwd);
|
|
25850
|
+
if (pushResult.code !== 0) return { message: `Push failed: ${pushResult.stderr}` };
|
|
25851
|
+
return { message: `${color.green("\u2713")} Pushed to ${remotes.join(", ")} (${branch})` };
|
|
25852
|
+
}
|
|
25853
|
+
};
|
|
25854
|
+
}
|
|
25855
|
+
|
|
25856
|
+
// src/plugins/observability-plugin.ts
|
|
25857
|
+
function createObservabilityPlugin(opts) {
|
|
25858
|
+
return {
|
|
25859
|
+
name: "wstack-observability",
|
|
25860
|
+
version: "1.0.0",
|
|
25861
|
+
description: "Runtime metrics and health checks: /metrics, /health",
|
|
25862
|
+
apiVersion: "^0.1",
|
|
25863
|
+
capabilities: { slashCommands: true },
|
|
25864
|
+
defaultConfig: {},
|
|
25865
|
+
setup(api) {
|
|
25866
|
+
const rawConfig = api.config;
|
|
25867
|
+
const metricsSink = opts?.metricsSink ?? rawConfig.metricsSink;
|
|
25868
|
+
const healthRegistry = opts?.healthRegistry ?? rawConfig.healthRegistry;
|
|
25869
|
+
api.slashCommands.register(buildMetricsCommand(metricsSink));
|
|
25870
|
+
api.slashCommands.register(buildHealthCommand(healthRegistry));
|
|
25871
|
+
api.log.info("[observability] loaded \u2014 /metrics, /health available");
|
|
25872
|
+
},
|
|
25873
|
+
teardown(api) {
|
|
25874
|
+
api.slashCommands.unregister("metrics");
|
|
25875
|
+
api.slashCommands.unregister("health");
|
|
25876
|
+
api.log.info("[observability] unloaded");
|
|
25877
|
+
},
|
|
25878
|
+
async health() {
|
|
25879
|
+
return { ok: true, message: "observability ready" };
|
|
25880
|
+
}
|
|
25881
|
+
};
|
|
25882
|
+
}
|
|
25883
|
+
function statusIcon(status) {
|
|
25884
|
+
if (status === "healthy") return color.green("\u25CF");
|
|
25885
|
+
if (status === "degraded") return color.yellow("\u25CF");
|
|
25886
|
+
return color.red("\u25CF");
|
|
25887
|
+
}
|
|
25888
|
+
function buildMetricsCommand(metricsSink) {
|
|
25889
|
+
return {
|
|
25890
|
+
name: "metrics",
|
|
25891
|
+
description: "Show metrics snapshot (requires --metrics flag).",
|
|
25892
|
+
async run() {
|
|
25893
|
+
if (!metricsSink) {
|
|
25894
|
+
return { message: "Metrics not enabled. Restart with --metrics to collect." };
|
|
25895
|
+
}
|
|
25896
|
+
const snap = metricsSink.snapshot();
|
|
25897
|
+
if (snap.series.length === 0) return { message: "No metrics recorded yet." };
|
|
25898
|
+
const lines = [];
|
|
25899
|
+
const byName = /* @__PURE__ */ new Map();
|
|
25900
|
+
for (const s of snap.series) {
|
|
25901
|
+
const bucket = byName.get(s.name) ?? [];
|
|
25902
|
+
bucket.push(s);
|
|
25903
|
+
byName.set(s.name, bucket);
|
|
25904
|
+
}
|
|
25905
|
+
for (const [name, series] of [...byName.entries()].sort()) {
|
|
25906
|
+
lines.push(color.dim(`# ${name}`));
|
|
25907
|
+
for (const s of series) {
|
|
25908
|
+
const labels = Object.entries(s.labels).map(([k, v]) => `${k}=${v}`).join(" ");
|
|
25909
|
+
const labelStr = labels ? color.dim(` {${labels}}`) : "";
|
|
25910
|
+
if (s.type === "histogram") {
|
|
25911
|
+
lines.push(
|
|
25912
|
+
` count=${s.values.count} sum=${s.values.sum} min=${s.values.min} max=${s.values.max} p50=${s.values.p50} p95=${s.values.p95} p99=${s.values.p99}${labelStr}`
|
|
25913
|
+
);
|
|
25914
|
+
} else {
|
|
25915
|
+
lines.push(` ${s.values.value}${labelStr}`);
|
|
25916
|
+
}
|
|
25917
|
+
}
|
|
25918
|
+
}
|
|
25919
|
+
return { message: lines.join("\n") };
|
|
25920
|
+
}
|
|
25921
|
+
};
|
|
25922
|
+
}
|
|
25923
|
+
function buildHealthCommand(healthRegistry) {
|
|
25924
|
+
return {
|
|
25925
|
+
name: "health",
|
|
25926
|
+
description: "Run health checks (requires --metrics flag).",
|
|
25927
|
+
async run() {
|
|
25928
|
+
if (!healthRegistry) {
|
|
25929
|
+
return { message: "Health checks not enabled. Restart with --metrics." };
|
|
25930
|
+
}
|
|
25931
|
+
const result = await healthRegistry.run();
|
|
25932
|
+
const lines = [
|
|
25933
|
+
`${statusIcon(result.status)} overall: ${result.status}`,
|
|
25934
|
+
...result.checks.map((c) => {
|
|
25935
|
+
const detail = c.detail ? color.dim(` \u2014 ${c.detail}`) : "";
|
|
25936
|
+
return ` ${statusIcon(c.status)} ${c.name}: ${c.status}${detail}`;
|
|
25937
|
+
})
|
|
25938
|
+
];
|
|
25939
|
+
return { message: lines.join("\n") };
|
|
25940
|
+
}
|
|
25941
|
+
};
|
|
25942
|
+
}
|
|
25943
|
+
|
|
25944
|
+
// src/plugins/security-plugin.ts
|
|
25945
|
+
function createSecurityPlugin() {
|
|
25946
|
+
return {
|
|
25947
|
+
name: "wstack-security",
|
|
25948
|
+
version: "1.0.0",
|
|
25949
|
+
description: "Security scanning: /security scan | audit | report",
|
|
25950
|
+
apiVersion: "^0.1",
|
|
25951
|
+
capabilities: { slashCommands: true },
|
|
25952
|
+
defaultConfig: {},
|
|
25953
|
+
setup(api) {
|
|
25954
|
+
api.slashCommands.register(createSecuritySlashCommand());
|
|
25955
|
+
api.log.info("[security] loaded \u2014 /security available");
|
|
25956
|
+
},
|
|
25957
|
+
teardown(api) {
|
|
25958
|
+
api.slashCommands.unregister("security");
|
|
25959
|
+
api.log.info("[security] unloaded");
|
|
25960
|
+
},
|
|
25961
|
+
async health() {
|
|
25962
|
+
return { ok: true, message: "security scanner ready" };
|
|
25963
|
+
}
|
|
25964
|
+
};
|
|
25965
|
+
}
|
|
25966
|
+
function createSkillsPlugin(opts) {
|
|
25967
|
+
return {
|
|
25968
|
+
name: "wstack-skills",
|
|
25969
|
+
version: "1.0.0",
|
|
25970
|
+
description: "Skill library and GitHub installer: /skill, /skill-gen, /skill-install, ...",
|
|
25971
|
+
apiVersion: "^0.1",
|
|
25972
|
+
capabilities: { slashCommands: true },
|
|
25973
|
+
defaultConfig: {},
|
|
25974
|
+
setup(api) {
|
|
25975
|
+
const rawConfig = api.config;
|
|
25976
|
+
const skillLoader = opts?.skillLoader ?? rawConfig.skillLoader;
|
|
25977
|
+
api.slashCommands.register(buildSkillCommand(skillLoader));
|
|
25978
|
+
api.slashCommands.register(buildSkillGeneratorCommand(skillLoader));
|
|
25979
|
+
api.slashCommands.register(buildSkillInstallCommand(skillLoader));
|
|
25980
|
+
api.slashCommands.register(buildSkillUpdateCommand(skillLoader));
|
|
25981
|
+
api.slashCommands.register(buildSkillUninstallCommand(skillLoader));
|
|
25982
|
+
api.log.info("[skills] loaded \u2014 /skill, /skill-gen, /skill-install/update/uninstall available");
|
|
25983
|
+
},
|
|
25984
|
+
teardown(api) {
|
|
25985
|
+
for (const name of ["skill", "skill-gen", "skill-install", "skill-update", "skill-uninstall"]) {
|
|
25986
|
+
api.slashCommands.unregister(name);
|
|
25987
|
+
}
|
|
25988
|
+
api.log.info("[skills] unloaded");
|
|
25989
|
+
},
|
|
25990
|
+
async health() {
|
|
25991
|
+
return { ok: true, message: "skills ready" };
|
|
25992
|
+
}
|
|
25993
|
+
};
|
|
25994
|
+
}
|
|
25995
|
+
function makeInstaller(skillLoader, projectRoot) {
|
|
25996
|
+
const globalRoot = path6.join(os5.homedir(), ".wrongstack");
|
|
25997
|
+
return new SkillInstaller({
|
|
25998
|
+
manifestPath: path6.join(globalRoot, "installed-skills.json"),
|
|
25999
|
+
projectSkillsDir: path6.join(projectRoot, ".wrongstack", "skills"),
|
|
26000
|
+
globalSkillsDir: path6.join(globalRoot, "skills"),
|
|
26001
|
+
projectHash: projectHash(projectRoot),
|
|
26002
|
+
skillLoader
|
|
26003
|
+
});
|
|
26004
|
+
}
|
|
26005
|
+
function buildSkillCommand(skillLoader) {
|
|
26006
|
+
return {
|
|
26007
|
+
name: "skill",
|
|
26008
|
+
description: "Show skill details or list available skills. Use /skill-gen to create new skills.",
|
|
26009
|
+
async run(args) {
|
|
26010
|
+
if (!skillLoader) return { message: "No skill loader configured." };
|
|
26011
|
+
if (!args.trim()) {
|
|
26012
|
+
const entries = await skillLoader.listEntries();
|
|
26013
|
+
if (entries.length === 0) return { message: "No skills found." };
|
|
26014
|
+
const lines = entries.map((e) => {
|
|
26015
|
+
const scopeTag = e.scope.length > 0 ? ` ${color.dim(`(${e.scope.slice(0, 3).join(", ")})`)}` : "";
|
|
26016
|
+
return ` ${color.bold(e.name)}${scopeTag}
|
|
26017
|
+
Use when: ${e.trigger}`;
|
|
26018
|
+
});
|
|
26019
|
+
return { message: `Available skills:
|
|
26020
|
+
${lines.join("\n\n")}
|
|
26021
|
+
` };
|
|
26022
|
+
}
|
|
26023
|
+
const skill = await skillLoader.find(args.trim());
|
|
26024
|
+
if (!skill) return { message: `Skill "${args.trim()}" not found.` };
|
|
26025
|
+
return { message: await skillLoader.readBody(skill.name) };
|
|
26026
|
+
}
|
|
26027
|
+
};
|
|
26028
|
+
}
|
|
26029
|
+
function buildSkillGeneratorCommand(skillLoader) {
|
|
26030
|
+
return {
|
|
26031
|
+
name: "skill-gen",
|
|
26032
|
+
description: "Create a new AI skill interactively. The AI will guide you.",
|
|
26033
|
+
help: [
|
|
26034
|
+
"\u2554\u2550\u2550\u2550 Skill Generator \u2550\u2550\u2550\u2557",
|
|
26035
|
+
"",
|
|
26036
|
+
"Create new AI skills with AI guidance.",
|
|
26037
|
+
"",
|
|
26038
|
+
"Usage:",
|
|
26039
|
+
" /skill-gen Start skill creation",
|
|
26040
|
+
" /skill-gen list List existing skills",
|
|
26041
|
+
" /skill-gen edit <name> View an existing skill",
|
|
26042
|
+
"",
|
|
26043
|
+
"The AI will ask you questions and create the skill file.",
|
|
26044
|
+
"Skills are saved to .wrongstack/skills/<name>/SKILL.md"
|
|
26045
|
+
].join("\n"),
|
|
26046
|
+
async run(args) {
|
|
26047
|
+
const trimmed = args.trim();
|
|
26048
|
+
if (trimmed === "list" || trimmed === "ls") {
|
|
26049
|
+
if (!skillLoader) return { message: "No skill loader configured." };
|
|
26050
|
+
const entries = await skillLoader.listEntries();
|
|
26051
|
+
if (entries.length === 0) return { message: "No skills found." };
|
|
26052
|
+
const lines = entries.map((e) => {
|
|
26053
|
+
const src = e.source === "project" ? "\u{1F4C1}" : e.source === "user" ? "\u{1F464}" : "\u{1F4E6}";
|
|
26054
|
+
return ` ${src} ${e.name}
|
|
26055
|
+
${e.trigger}`;
|
|
26056
|
+
});
|
|
26057
|
+
return { message: `Available Skills:
|
|
26058
|
+
${lines.join("\n\n")}
|
|
26059
|
+
` };
|
|
26060
|
+
}
|
|
26061
|
+
if (trimmed.startsWith("edit ")) {
|
|
26062
|
+
const skillName = trimmed.slice(5).trim();
|
|
26063
|
+
if (!skillLoader) return { message: "No skill loader configured." };
|
|
26064
|
+
const skill = await skillLoader.find(skillName);
|
|
26065
|
+
if (!skill) return { message: `Skill "${skillName}" not found.` };
|
|
26066
|
+
const body = await skillLoader.readBody(skillName);
|
|
26067
|
+
return { message: [`Skill: ${skillName}`, `Path: ${skill.path}`, "", body].join("\n") };
|
|
26068
|
+
}
|
|
26069
|
+
return {
|
|
26070
|
+
message: "\u2554\u2550\u2550\u2550 Skill Generator \u2550\u2550\u2550\u2557\n\nThe AI will guide you through creating a new skill.\nAnswer its questions naturally.",
|
|
26071
|
+
runText: "I want to create a new AI skill. Read the skill-creator skill and guide me through the process. Ask me questions one at a time \u2014 name, description, what to cover \u2014 then create the SKILL.md file."
|
|
26072
|
+
};
|
|
26073
|
+
}
|
|
26074
|
+
};
|
|
26075
|
+
}
|
|
26076
|
+
function buildSkillInstallCommand(skillLoader) {
|
|
26077
|
+
return {
|
|
26078
|
+
name: "skill-install",
|
|
26079
|
+
description: "Install skills from a GitHub repository.",
|
|
26080
|
+
argsHint: "<user/repo[@ref]> [--global]",
|
|
26081
|
+
help: [
|
|
26082
|
+
"\u2554\u2550\u2550\u2550 Skill Install \u2550\u2550\u2550\u2557",
|
|
26083
|
+
"",
|
|
26084
|
+
"Install skills from a GitHub repository.",
|
|
26085
|
+
"",
|
|
26086
|
+
"Usage:",
|
|
26087
|
+
" /skill-install <user/repo> Install from default branch (main)",
|
|
26088
|
+
" /skill-install <user/repo@ref> Install specific tag/branch/commit",
|
|
26089
|
+
" /skill-install <user/repo> --global Install to user-global skills",
|
|
26090
|
+
"",
|
|
26091
|
+
"Supports both single-skill repos (SKILL.md at root)",
|
|
26092
|
+
"and multi-skill repos (skills/ subdirectory).",
|
|
26093
|
+
"",
|
|
26094
|
+
"Examples:",
|
|
26095
|
+
" /skill-install wrongstack/awesome-skills",
|
|
26096
|
+
" /skill-install wrongstack/skills@v1.0",
|
|
26097
|
+
" /skill-install user/my-skills --global"
|
|
26098
|
+
].join("\n"),
|
|
26099
|
+
async run(args, ctx) {
|
|
26100
|
+
const parts = args.trim().split(/\s+/);
|
|
26101
|
+
const ref = parts.find((p) => !p.startsWith("--"));
|
|
26102
|
+
const isGlobal = parts.includes("--global");
|
|
26103
|
+
if (!ref) return { message: "Usage: /skill-install <user/repo[@ref]> [--global]" };
|
|
26104
|
+
const installer = makeInstaller(skillLoader, ctx.projectRoot);
|
|
26105
|
+
try {
|
|
26106
|
+
const results = await installer.install(ref, { global: isGlobal });
|
|
26107
|
+
if (results.length === 0) return { message: "No skills found in the repository." };
|
|
26108
|
+
const scope = isGlobal ? "user-global" : "project";
|
|
26109
|
+
const lines = [`Installed ${results.length} skill(s) [${scope}]:`];
|
|
26110
|
+
for (const r of results) {
|
|
26111
|
+
lines.push(` \u2713 ${r.name} (${r.source}@${r.ref})`);
|
|
26112
|
+
lines.push(` \u2192 ${r.path}`);
|
|
26113
|
+
}
|
|
26114
|
+
return { message: lines.join("\n") };
|
|
26115
|
+
} catch (err) {
|
|
26116
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
26117
|
+
return { message: `\u2717 Install failed: ${msg}` };
|
|
26118
|
+
}
|
|
26119
|
+
}
|
|
26120
|
+
};
|
|
26121
|
+
}
|
|
26122
|
+
function buildSkillUpdateCommand(skillLoader) {
|
|
26123
|
+
return {
|
|
26124
|
+
name: "skill-update",
|
|
26125
|
+
description: "Update installed skills from their GitHub source.",
|
|
26126
|
+
argsHint: "[name|ref] [--global]",
|
|
26127
|
+
help: [
|
|
26128
|
+
"\u2554\u2550\u2550\u2550 Skill Update \u2550\u2550\u2550\u2557",
|
|
26129
|
+
"",
|
|
26130
|
+
"Update installed skills from their GitHub source.",
|
|
26131
|
+
"",
|
|
26132
|
+
"Usage:",
|
|
26133
|
+
" /skill-update Update all installed skills",
|
|
26134
|
+
" /skill-update <name> Update a specific skill",
|
|
26135
|
+
" /skill-update <user/repo@ref> Update to a different ref",
|
|
26136
|
+
" /skill-update <name> --global Update a global skill"
|
|
26137
|
+
].join("\n"),
|
|
26138
|
+
async run(args, ctx) {
|
|
26139
|
+
const parts = args.trim().split(/\s+/);
|
|
26140
|
+
const nameOrRef = parts.find((p) => !p.startsWith("--"));
|
|
26141
|
+
const isGlobal = parts.includes("--global");
|
|
26142
|
+
const installer = makeInstaller(skillLoader, ctx.projectRoot);
|
|
26143
|
+
try {
|
|
26144
|
+
const result = await installer.update(nameOrRef, { global: isGlobal });
|
|
26145
|
+
const lines = [];
|
|
26146
|
+
if (result.updated.length > 0) {
|
|
26147
|
+
lines.push(`Updated ${result.updated.length} skill(s):`);
|
|
26148
|
+
for (const u of result.updated) {
|
|
26149
|
+
lines.push(
|
|
26150
|
+
u.oldRef !== u.newRef ? ` \u2713 ${u.name} (${u.oldRef} \u2192 ${u.newRef})` : ` \u2713 ${u.name} (refreshed)`
|
|
26151
|
+
);
|
|
26152
|
+
}
|
|
26153
|
+
}
|
|
26154
|
+
if (result.unchanged.length > 0) lines.push(`Up to date: ${result.unchanged.join(", ")}`);
|
|
26155
|
+
if (result.errors.length > 0) {
|
|
26156
|
+
for (const e of result.errors) lines.push(` \u2717 ${e.name}: ${e.error}`);
|
|
26157
|
+
}
|
|
26158
|
+
if (lines.length === 0) return { message: "No installed skills to update." };
|
|
26159
|
+
return { message: lines.join("\n") };
|
|
26160
|
+
} catch (err) {
|
|
26161
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
26162
|
+
return { message: `\u2717 Update failed: ${msg}` };
|
|
26163
|
+
}
|
|
26164
|
+
}
|
|
26165
|
+
};
|
|
26166
|
+
}
|
|
26167
|
+
function buildSkillUninstallCommand(skillLoader) {
|
|
26168
|
+
return {
|
|
26169
|
+
name: "skill-uninstall",
|
|
26170
|
+
description: "Remove an installed skill.",
|
|
26171
|
+
argsHint: "<name> [--global]",
|
|
26172
|
+
help: [
|
|
26173
|
+
"\u2554\u2550\u2550\u2550 Skill Uninstall \u2550\u2550\u2550\u2557",
|
|
26174
|
+
"",
|
|
26175
|
+
"Remove an installed skill and its files.",
|
|
26176
|
+
"",
|
|
26177
|
+
"Usage:",
|
|
26178
|
+
" /skill-uninstall <name> Remove from project skills",
|
|
26179
|
+
" /skill-uninstall <name> --global Remove from user-global skills"
|
|
26180
|
+
].join("\n"),
|
|
26181
|
+
async run(args, ctx) {
|
|
26182
|
+
const parts = args.trim().split(/\s+/);
|
|
26183
|
+
const name = parts.find((p) => !p.startsWith("--"));
|
|
26184
|
+
const isGlobal = parts.includes("--global");
|
|
26185
|
+
if (!name) {
|
|
26186
|
+
const installer2 = makeInstaller(skillLoader, ctx.projectRoot);
|
|
26187
|
+
const installed = await installer2.listInstalled();
|
|
26188
|
+
if (installed.length === 0) return { message: "No installed skills found." };
|
|
26189
|
+
const scope = isGlobal ? "user" : "project";
|
|
26190
|
+
const filtered = installed.filter((s) => s.scope === scope);
|
|
26191
|
+
if (filtered.length === 0) {
|
|
26192
|
+
return { message: `No installed skills found (${scope} scope).` };
|
|
26193
|
+
}
|
|
26194
|
+
const lines = [`Installed skills (${scope}):`];
|
|
26195
|
+
for (const s of filtered) {
|
|
26196
|
+
lines.push(` ${s.name} ${s.source}@${s.ref} (${s.installedAt.slice(0, 10)})`);
|
|
26197
|
+
}
|
|
26198
|
+
lines.push("", "Use /skill-uninstall <name> to remove.");
|
|
26199
|
+
return { message: lines.join("\n") };
|
|
26200
|
+
}
|
|
26201
|
+
const installer = makeInstaller(skillLoader, ctx.projectRoot);
|
|
26202
|
+
try {
|
|
26203
|
+
await installer.uninstall(name, { global: isGlobal });
|
|
26204
|
+
return { message: `\u2713 Skill "${name}" uninstalled.` };
|
|
26205
|
+
} catch (err) {
|
|
26206
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
26207
|
+
return { message: `\u2717 Uninstall failed: ${msg}` };
|
|
26208
|
+
}
|
|
26209
|
+
}
|
|
26210
|
+
};
|
|
26211
|
+
}
|
|
26212
|
+
|
|
26213
|
+
// src/plugins/plan-plugin.ts
|
|
26214
|
+
function createPlanPlugin(opts) {
|
|
26215
|
+
return {
|
|
26216
|
+
name: "wstack-plan",
|
|
26217
|
+
version: "1.0.0",
|
|
26218
|
+
description: "Strategic plan board: /plan show | add | start | done | promote | template | clear",
|
|
26219
|
+
apiVersion: "^0.1",
|
|
26220
|
+
capabilities: { slashCommands: true },
|
|
26221
|
+
defaultConfig: {},
|
|
26222
|
+
setup(api) {
|
|
26223
|
+
const rawConfig = api.config;
|
|
26224
|
+
const paths = opts?.paths ?? rawConfig.paths;
|
|
26225
|
+
api.slashCommands.register(buildPlanCommand(paths?.projectPlan));
|
|
26226
|
+
api.log.info("[plan] loaded \u2014 /plan available");
|
|
26227
|
+
},
|
|
26228
|
+
teardown(api) {
|
|
26229
|
+
api.slashCommands.unregister("plan");
|
|
26230
|
+
api.log.info("[plan] unloaded");
|
|
26231
|
+
},
|
|
26232
|
+
async health() {
|
|
26233
|
+
return { ok: true, message: "plan board ready" };
|
|
26234
|
+
}
|
|
26235
|
+
};
|
|
26236
|
+
}
|
|
26237
|
+
function buildPlanCommand(planPath) {
|
|
26238
|
+
return {
|
|
26239
|
+
name: "plan",
|
|
26240
|
+
description: "Strategic plan board: /plan [show|add <title>|start <id|#>|done <id|#>|remove <id|#>|promote <id|#> [subtask ...]|derive <id|#>|template [list|use <name>]|clear]",
|
|
26241
|
+
async run(args, ctx) {
|
|
26242
|
+
if (!planPath) return { message: "Plan storage is not configured for this session." };
|
|
26243
|
+
const sessionId = ctx?.session?.id ?? "unknown";
|
|
26244
|
+
const [verb, ...rest] = args.trim().split(/\s+/);
|
|
26245
|
+
const restJoined = rest.join(" ").trim();
|
|
26246
|
+
const plan = await loadPlan(planPath) ?? emptyPlan(sessionId);
|
|
26247
|
+
switch (verb) {
|
|
26248
|
+
case "":
|
|
26249
|
+
case "show":
|
|
26250
|
+
case "list":
|
|
26251
|
+
return { message: formatPlan(plan) };
|
|
26252
|
+
case "add": {
|
|
26253
|
+
if (!restJoined) return { message: "Usage: /plan add <title>" };
|
|
26254
|
+
const { plan: updated, item } = addPlanItem(plan, restJoined);
|
|
26255
|
+
await savePlan(planPath, updated);
|
|
26256
|
+
return { message: `Added: ${item.title}
|
|
26257
|
+
${formatPlan(updated)}` };
|
|
26258
|
+
}
|
|
26259
|
+
case "start":
|
|
26260
|
+
case "progress": {
|
|
26261
|
+
if (!restJoined) return { message: "Usage: /plan start <id|index>" };
|
|
26262
|
+
const updated = setPlanItemStatus(plan, restJoined, "in_progress");
|
|
26263
|
+
await savePlan(planPath, updated);
|
|
26264
|
+
return { message: formatPlan(updated) };
|
|
26265
|
+
}
|
|
26266
|
+
case "done":
|
|
26267
|
+
case "complete": {
|
|
26268
|
+
if (!restJoined) return { message: "Usage: /plan done <id|index>" };
|
|
26269
|
+
const updated = setPlanItemStatus(plan, restJoined, "done");
|
|
26270
|
+
await savePlan(planPath, updated);
|
|
26271
|
+
return { message: formatPlan(updated) };
|
|
26272
|
+
}
|
|
26273
|
+
case "remove":
|
|
26274
|
+
case "delete":
|
|
26275
|
+
case "rm": {
|
|
26276
|
+
if (!restJoined) return { message: "Usage: /plan remove <id|index>" };
|
|
26277
|
+
const updated = removePlanItem(plan, restJoined);
|
|
26278
|
+
await savePlan(planPath, updated);
|
|
26279
|
+
return { message: formatPlan(updated) };
|
|
26280
|
+
}
|
|
26281
|
+
case "promote": {
|
|
26282
|
+
if (!restJoined) return { message: "Usage: /plan promote <id|index> [subtask ...]" };
|
|
26283
|
+
const [target, ...subtasks] = restJoined.split(/\s+/);
|
|
26284
|
+
if (!target) return { message: "Usage: /plan promote <id|index> [subtask ...]" };
|
|
26285
|
+
const derived = deriveTodosFromPlanItem(plan, target, subtasks.length > 0 ? subtasks : void 0);
|
|
26286
|
+
if (!derived) return { message: `No plan item matched "${target}".` };
|
|
26287
|
+
await savePlan(planPath, derived.plan);
|
|
26288
|
+
ctx?.state?.replaceTodos(derived.todos);
|
|
26289
|
+
return {
|
|
26290
|
+
message: `Promoted to ${derived.todos.length} todo(s):
|
|
26291
|
+
${formatTodosList(derived.todos)}
|
|
26292
|
+
|
|
26293
|
+
${formatPlan(derived.plan)}`
|
|
26294
|
+
};
|
|
26295
|
+
}
|
|
26296
|
+
case "derive": {
|
|
26297
|
+
if (!restJoined) return { message: "Usage: /plan derive <id|index>" };
|
|
26298
|
+
const derived = deriveTodosFromPlanItem(plan, restJoined);
|
|
26299
|
+
if (!derived) return { message: `No plan item matched "${restJoined}".` };
|
|
26300
|
+
await savePlan(planPath, derived.plan);
|
|
26301
|
+
ctx?.state?.replaceTodos(derived.todos);
|
|
26302
|
+
return {
|
|
26303
|
+
message: `Derived ${derived.todos.length} todo(s):
|
|
26304
|
+
${formatTodosList(derived.todos)}
|
|
26305
|
+
|
|
26306
|
+
${formatPlan(derived.plan)}`
|
|
26307
|
+
};
|
|
26308
|
+
}
|
|
26309
|
+
case "template": {
|
|
26310
|
+
const subVerb = rest[0] ?? "";
|
|
26311
|
+
const subRest = rest.slice(1).join(" ").trim();
|
|
26312
|
+
if (subVerb === "" || subVerb === "list") return { message: formatPlanTemplates() };
|
|
26313
|
+
if (subVerb === "use") {
|
|
26314
|
+
if (!subRest) return { message: "Usage: /plan template use <template-name>" };
|
|
26315
|
+
const template = getPlanTemplate(subRest);
|
|
26316
|
+
if (!template) {
|
|
26317
|
+
return {
|
|
26318
|
+
message: `Unknown template "${subRest}". Use /plan template list to see available templates.`
|
|
26319
|
+
};
|
|
26320
|
+
}
|
|
26321
|
+
let updated = plan;
|
|
26322
|
+
for (const item of template.items) {
|
|
26323
|
+
({ plan: updated } = addPlanItem(updated, item.title, item.details));
|
|
26324
|
+
}
|
|
26325
|
+
await savePlan(planPath, updated);
|
|
26326
|
+
return {
|
|
26327
|
+
message: `Applied template "${template.name}" (${template.items.length} items):
|
|
26328
|
+
${formatPlan(updated)}`
|
|
26329
|
+
};
|
|
26330
|
+
}
|
|
26331
|
+
return { message: `Unknown template subcommand "${subVerb}". Try: list | use <name>` };
|
|
26332
|
+
}
|
|
26333
|
+
case "clear": {
|
|
26334
|
+
const updated = clearPlan(plan);
|
|
26335
|
+
await savePlan(planPath, updated);
|
|
26336
|
+
return { message: "Plan cleared." };
|
|
26337
|
+
}
|
|
26338
|
+
default:
|
|
26339
|
+
return {
|
|
26340
|
+
message: `Unknown subcommand "${verb}". Try: show | add <title> | start <id|#> | done <id|#> | remove <id|#> | promote <id|#> | derive <id|#> | template [list|use <name>] | clear`
|
|
26341
|
+
};
|
|
26342
|
+
}
|
|
26343
|
+
}
|
|
26344
|
+
};
|
|
26345
|
+
}
|
|
26346
|
+
|
|
26347
|
+
export { AGENTS_BY_PHASE, AGENT_CATALOG, AISpecBuilder, ALL_AGENT_DEFINITIONS, ALL_FLEET_AGENTS, ALL_SYNC_CATEGORIES, AUDIT_LOG_AGENT, Agent, AgentError, AutoApprovePermissionPolicy, AutoCompactionMiddleware, AutoExecutor, AutoPhasePlanner, AutoPhaseRunner, AutonomousRunner, BUG_HUNTER_AGENT, BudgetExceededError, CONTEXT_WINDOW_MODES, CheckpointManager, CloudSync, ConfigError, ConfigMigrationError, Container, Context, ConversationState, DEFAULT_AUTONOMY_CONFIG, DEFAULT_CONFIG_MIGRATIONS, DEFAULT_CONTEXT_CONFIG, DEFAULT_CONTEXT_WINDOW_MODE_ID, DEFAULT_DIRECTOR_PREAMBLE, DEFAULT_DISPATCH_ROLE, DEFAULT_MAX_ITERATIONS, DEFAULT_MODES, DEFAULT_RECOVERY_STRATEGIES, DEFAULT_SPEC_TEMPLATE, DEFAULT_SUBAGENT_BASELINE, DEFAULT_TOOLS_CONFIG, DefaultAttachmentStore, DefaultConfigLoader, DefaultConfigStore, DefaultErrorHandler, DefaultHealthRegistry, DefaultLogger, DefaultMemoryStore, DefaultModeStore, DefaultModelsRegistry, DefaultMultiAgentCoordinator, DefaultPathResolver, DefaultPermissionPolicy, DefaultPluginAPI, DefaultPromptStore, DefaultProviderRunner, DefaultRetryPolicy, DefaultSecretScrubber, DefaultSecretVault, DefaultSessionReader, DefaultSessionRewinder, DefaultSessionStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultTaskStore, DefaultTokenCounter, Director, DirectorStateCheckpoint, DoneConditionChecker, ERROR_CODES, EternalAutonomyEngine, EventBus, ExtensionRegistry, FLEET_ROSTER, FLEET_ROSTER_BUDGETS, FleetBus, FleetManager, FleetSpawnBudgetError, FleetUsageAggregator, FsError, GitignoreUpdater, HybridCompactor, InMemoryAgentBridge, InMemoryBridgeTransport, InMemoryMetricsSink, InputBuilder, IntelligentCompactor, KERNEL_API_VERSION, LAYER_1_IDENTITY, LLMSelector, MAX_JOURNAL_ENTRIES, NULL_FLEET_BUS, NoopMetricsSink, NoopTracer, OTelTracer, PROMETHEUS_CONTENT_TYPE, ParallelEternalEngine, PhaseGraphBuilder, PhaseOrchestrator, PhaseStore, Pipeline, PluginError, ProviderError, ProviderRegistry, QueueStore, REFACTOR_PLANNER_AGENT, RecoveryLock, ReportGenerator, RunController, SECURITY_SCANNER_AGENT, SPEC_TEMPLATES, ScopedEventBus, SddParallelRun, SddTaskDecomposer, SecurityScanner, SecurityScannerOrchestrator, SelectiveCompactor, SessionAnalyzer, SessionError, SkillGenerator, SkillInstaller, SkillManifestStore, SlashCommandRegistry, SpecDrivenDev, SpecParser, SpecStore, SpecVersioning, SubagentBudget, TOKENS, TaskFlow, TaskGenerator, TaskGraphStore, TaskTracker, TechStackDetector, ToolError, ToolExecutor, ToolRegistry, WorktreeManager, WrongStackError, addPlanItem, allServers, analyzeCriticalPath, appendJournal, applyRosterBudget, asBlocks, asText, assertSafePath, atomicWrite, attachAutoExtend, attachPlanCheckpoint, attachTodosCheckpoint, awsServer, blockServer, braveSearchServer, buildBtwBlock, buildChildEnv, buildGoalPreamble, buildOtlpMetricsRequest, buildOtlpTracesRequest, buildRecoveryStrategies, classifyFamily, clearPlan, color, compileGlob, compileUserRegex, composeDirectorPrompt, composeSubagentPrompt, computeTaskProgress, consumeBtwNotes, context7Server, contextManagerTool, createAutoExecutor, createAutoPhaseFromTaskGraph, createContextManagerTool, createDefaultPipelines, createDelegateTool, createGitPlugin, createMcpControlTool, createMessage, createObservabilityPlugin, createPlanPlugin, createPromptsPlugin, createSecurityPlugin, createSecuritySlashCommand, createSkillsPlugin, createSyncPlugin, createToolOutputSerializer, decryptConfigSecrets, defaultGitignoreUpdater, defaultOrchestrator, defaultReportGenerator, defaultSecurityScanner, defaultSkillGenerator, defaultTechStackDetector, deriveTodosFromPlanItem, detectNewlineStyle, dispatchAgent, downloadGitHubTarball, emptyGoal, emptyPlan, encryptConfigSecrets, ensureDir, estimateRequestTokens, estimateTextTokens, estimateToolDefTokens, estimateToolInputTokens, estimateToolResultTokens, everArtServer, extractRunEnv, filesystemServer, findCriticalPath, formatContextWindowModeList, formatGoal, formatPlan, formatPlanTemplates, formatTodosList, getAgentDefinition, getContextWindowMode, getPlanTemplate, getTemplate, githubServer, goalFilePath, googleMapsServer, isAgentError, isConfigError, isContextWindowModeId, isFsError, isImageBlock, isPluginError, isSessionError, isTextBlock, isThinkingBlock, isToolError, isToolResultBlock, isToolUseBlock, isWrongStackError, listContextWindowModes, listPlanTemplates, listTemplates, loadDirectorState, loadGoal, loadPlan, loadPlugins, loadProjectModes, loadTodosCheckpoint, loadUserModes, makeAgentSubagentRunner, makeAutonomyPromptContributor, makeContinueToNextIterationTool, makeDirectorSessionFactory, makeLLMClassifier, matchAny, matchGlob, migratePlaintextSecrets, miniMaxVisionServer, normalizeToLf, parseContinueDirective, parseSkillRef, pendingBtwCount, projectHash, removePlanItem, renderProgress, renderPrometheus, renderSpecAnalysis, renderTaskGraph, renderTaskList, repairToolUseAdjacency, resolveContextWindowPolicy, resolveWstackPaths, rewriteConfigEncrypted, rosterSummaryFromConfigs, runConfigMigrations, safeParse, safeStringify, sanitizeJsonString, saveGoal, savePlan, saveTodosCheckpoint, scoreAgents, securitySlashCommand, sentinelServer, setBtwNote, setPlanItemStatus, slackServer, startMetricsServer, startOtlpMetricsExporter, startOtlpTraceExporter, stripAnsi, summarizeUsage, templateToMarkdown, toStyle, toWrongStackError, topologicalSort, unifiedDiff, unloadPlugins, validateAgainstSchema, wireMetricsToEvents, wrapAsState, zaiVisionServer };
|
|
24931
26348
|
//# sourceMappingURL=index.js.map
|
|
24932
26349
|
//# sourceMappingURL=index.js.map
|