pstack-dsh 0.2.3
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/HARNESS.md +109 -0
- package/LICENSE +25 -0
- package/README.md +168 -0
- package/TEST-PLAN.md +70 -0
- package/agents/comment-sicko.md +32 -0
- package/agents/poteto-agent.md +9 -0
- package/cordis.patch.yml +4 -0
- package/cordis.yml +4 -0
- package/docs/guide/01-setup.md +41 -0
- package/docs/guide/02-poteto-mode.md +98 -0
- package/docs/guide/03-understand.md +53 -0
- package/docs/guide/04-design.md +81 -0
- package/docs/guide/05-build-and-clean.md +75 -0
- package/docs/guide/06-verify-and-ship.md +87 -0
- package/docs/guide/07-overnight.md +81 -0
- package/docs/guide/08-principles.md +71 -0
- package/docs/guide/09-make-it-yours.md +67 -0
- package/docs/guide/10-recipes-and-pitfalls.md +96 -0
- package/docs/guide/README.md +30 -0
- package/dshx.yml +9 -0
- package/lib/client.js +981 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +34 -0
- package/lib/index.js +1120 -0
- package/package.json +140 -0
- package/skills/architect/SKILL.md +83 -0
- package/skills/architect/references/design-red-flags.md +33 -0
- package/skills/architect/references/rationale-template.md +35 -0
- package/skills/architect/references/runner-prompt.md +20 -0
- package/skills/arena/SKILL.md +73 -0
- package/skills/automate-me/SKILL.md +103 -0
- package/skills/blast-radius/SKILL.md +50 -0
- package/skills/bro/SKILL.md +7 -0
- package/skills/create-verification-skill/SKILL.md +44 -0
- package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
- package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
- package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
- package/skills/figure-it-out/SKILL.md +53 -0
- package/skills/how/SKILL.md +45 -0
- package/skills/how/references/explainer-prompt.md +55 -0
- package/skills/how/references/explorer-prompt.md +52 -0
- package/skills/interrogate/SKILL.md +99 -0
- package/skills/interrogate/references/code-quality-review.md +47 -0
- package/skills/interrogate/references/lead-judgment.md +58 -0
- package/skills/interrogate/references/reviewer-prompt.md +72 -0
- package/skills/interrogate/references/rubric.md +77 -0
- package/skills/maintain-verification-skill/SKILL.md +39 -0
- package/skills/no-comments/SKILL.md +24 -0
- package/skills/poteto-mode/SKILL.md +149 -0
- package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
- package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-stack.md +16 -0
- package/skills/poteto-mode/playbooks/babysit.md +27 -0
- package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
- package/skills/poteto-mode/playbooks/eval.md +25 -0
- package/skills/poteto-mode/playbooks/feature.md +21 -0
- package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
- package/skills/poteto-mode/playbooks/investigation.md +14 -0
- package/skills/poteto-mode/playbooks/multi-phase-plan.md +156 -0
- package/skills/poteto-mode/playbooks/opening-a-pr.md +33 -0
- package/skills/poteto-mode/playbooks/orchestrate.md +113 -0
- package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
- package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
- package/skills/poteto-mode/playbooks/prototype.md +14 -0
- package/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/skills/poteto-mode/playbooks/session-pickup.md +11 -0
- package/skills/poteto-mode/playbooks/shipping.md +17 -0
- package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
- package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
- package/skills/poteto-mode/references/bugbot-triage.md +142 -0
- package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
- package/skills/poteto-mode/scripts/bun.lock +67 -0
- package/skills/poteto-mode/scripts/check-plan.mjs +186 -0
- package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
- package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
- package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
- package/skills/poteto-mode/scripts/package.json +16 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
- package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
- package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
- package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
- package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
- package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
- package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
- package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
- package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
- package/skills/poteto-mode/scripts/worktree-audit.sh +86 -0
- package/skills/principle-attack-the-premise/SKILL.md +22 -0
- package/skills/principle-boundary-discipline/SKILL.md +33 -0
- package/skills/principle-build-the-lever/SKILL.md +22 -0
- package/skills/principle-encode-lessons-in-structure/SKILL.md +30 -0
- package/skills/principle-exhaust-the-design-space/SKILL.md +20 -0
- package/skills/principle-experience-first/SKILL.md +18 -0
- package/skills/principle-fix-root-causes/SKILL.md +22 -0
- package/skills/principle-foundational-thinking/SKILL.md +20 -0
- package/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/skills/principle-laziness-protocol/SKILL.md +17 -0
- package/skills/principle-make-operations-idempotent/SKILL.md +23 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +21 -0
- package/skills/principle-minimize-reader-load/SKILL.md +22 -0
- package/skills/principle-model-the-domain/SKILL.md +25 -0
- package/skills/principle-never-block-on-the-human/SKILL.md +21 -0
- package/skills/principle-outcome-oriented-execution/SKILL.md +21 -0
- package/skills/principle-prove-it-works/SKILL.md +32 -0
- package/skills/principle-redesign-from-first-principles/SKILL.md +15 -0
- package/skills/principle-separate-before-serializing-shared-state/SKILL.md +15 -0
- package/skills/principle-sequence-verifiable-units/SKILL.md +21 -0
- package/skills/principle-subtract-before-you-add/SKILL.md +20 -0
- package/skills/principle-test-behavior-not-implementation/SKILL.md +24 -0
- package/skills/principle-type-system-discipline/SKILL.md +30 -0
- package/skills/recall/SKILL.md +35 -0
- package/skills/reflect/SKILL.md +65 -0
- package/skills/reflect/references/divergent-reviewer.md +43 -0
- package/skills/reflect/references/judgment-reviewer.md +42 -0
- package/skills/reflect/references/synthesizer.md +56 -0
- package/skills/reflect/references/tooling-reviewer.md +55 -0
- package/skills/setup-pstack/SKILL.md +32 -0
- package/skills/setup-pstack/references/catalog.md +28 -0
- package/skills/setup-pstack/references/spawn.md +61 -0
- package/skills/show-me-your-work/SKILL.md +82 -0
- package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
- package/skills/show-me-your-work/scripts/log.sh +40 -0
- package/skills/swarm/SKILL.md +44 -0
- package/skills/tdd/SKILL.md +44 -0
- package/skills/teach/SKILL.md +21 -0
- package/skills/technical-writing/SKILL.md +127 -0
- package/skills/typescript-best-practices/SKILL.md +30 -0
- package/skills/typescript-best-practices/references/patterns.md +313 -0
- package/skills/unslop/SKILL.md +67 -0
- package/skills/why/SKILL.md +146 -0
- package/skills/why/references/epistemics.md +144 -0
- package/skills/why/references/investigator-prompt.md +103 -0
- package/skills/why/references/source-playbook.md +17 -0
- package/skills/why/references/sources/code-archaeology.md +88 -0
- package/skills/why/references/sources/databricks.md +70 -0
- package/skills/why/references/sources/datadog.md +99 -0
- package/skills/why/references/sources/incident-postmortem.md +15 -0
- package/skills/why/references/sources/linear.md +48 -0
- package/skills/why/references/sources/notion.md +55 -0
- package/skills/why/references/sources/sentry.md +100 -0
- package/skills/why/references/sources/slack.md +54 -0
- package/skills/why/references/synthesizer-prompt.md +135 -0
- package/src/catalog-types.ts +54 -0
- package/src/catalog.ts +308 -0
- package/src/client/PstackSettings.tsx +519 -0
- package/src/client/api.ts +56 -0
- package/src/client/index.tsx +38 -0
- package/src/client/locales.ts +65 -0
- package/src/client/poteto-defaults.ts +37 -0
- package/src/frontmatter.ts +73 -0
- package/src/home.ts +13 -0
- package/src/http-trust.ts +38 -0
- package/src/ids.ts +39 -0
- package/src/index.ts +85 -0
- package/src/overlay-model.ts +139 -0
- package/src/overlay.ts +40 -0
- package/src/persona.ts +38 -0
- package/src/plugin-config.ts +29 -0
- package/src/request-overlay.ts +40 -0
- package/src/roles.ts +55 -0
- package/src/settings-api.ts +48 -0
- package/src/settings-draft.ts +144 -0
- package/src/settings-routes.ts +77 -0
- package/src/skills-provider.ts +96 -0
- package/src/spawn.ts +52 -0
- package/src/tools.ts +226 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
2
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
3
|
+
import { snapshotJsonValue } from "@deepseek-ai/dsh-util-values";
|
|
4
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
5
|
+
import { dirname, join, resolve } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
|
|
9
|
+
//#region src/ids.ts
|
|
10
|
+
/** Cordis plugin / Loader row id. */
|
|
11
|
+
const PLUGIN_ID = "pstack-dsh";
|
|
12
|
+
/** Bundled skill provider name on `ctx.skills`. */
|
|
13
|
+
const SKILL_PROVIDER_NAME = "pstack-dsh";
|
|
14
|
+
/** Overlay file under `$DSH_HOME`. */
|
|
15
|
+
const OVERLAY_FILENAME = "pstack-dsh.json";
|
|
16
|
+
/** DSH-owned OAuth store from dsh-oauth-login. Never ~/.pi / ~/.codex / ~/.claude / grok CLI. */
|
|
17
|
+
const OAUTH_AUTH_FILENAME = ".dsh-oauth-auth.json";
|
|
18
|
+
const OAUTH_AUTH_LEGACY_FILENAME = ".pi-login-auth.json";
|
|
19
|
+
/** Antigravity OAuth store from dsh-antigravity-oauth. */
|
|
20
|
+
const ANTIGRAVITY_AUTH_FILENAME = ".dsh-antigravity-oauth.json";
|
|
21
|
+
/** Official DeepSeek adapter. `packages/llm/llm-deepseek/src/index.ts` `PROVIDER`, `DEFAULT_API_KEY_ENV`. */
|
|
22
|
+
const DEEPSEEK_API_KEY_ENV = "DEEPSEEK_API_KEY";
|
|
23
|
+
/** Model-facing tool names this plugin registers. */
|
|
24
|
+
const TOOL_CATALOG = "pstack_catalog";
|
|
25
|
+
const TOOL_SPAWN = "pstack_spawn";
|
|
26
|
+
const TOOL_OVERLAY_READ = "pstack_overlay_read";
|
|
27
|
+
const TOOL_OVERLAY_WRITE = "pstack_overlay_write";
|
|
28
|
+
/** Same-origin Web routes. Pattern: dsh-oauth-login `src/auth-routes.ts`. */
|
|
29
|
+
const SETTINGS_SNAPSHOT_PATH = "/plugins/pstack-dsh/settings";
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/plugin-config.ts
|
|
32
|
+
const Config = { "~standard": {
|
|
33
|
+
version: 1,
|
|
34
|
+
vendor: "pstack-dsh",
|
|
35
|
+
validate(value) {
|
|
36
|
+
if (value === void 0 || value === null) return { value: { spawnProvider: "spawn" } };
|
|
37
|
+
if (typeof value !== "object" || Array.isArray(value)) return { issues: [{ message: "config must be an object" }] };
|
|
38
|
+
const spawnProvider = value.spawnProvider;
|
|
39
|
+
if (spawnProvider !== void 0 && (typeof spawnProvider !== "string" || spawnProvider.length === 0)) return { issues: [{ message: "spawnProvider must be a non-empty string" }] };
|
|
40
|
+
return { value: { spawnProvider: spawnProvider ?? "spawn" } };
|
|
41
|
+
}
|
|
42
|
+
} };
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/frontmatter.ts
|
|
45
|
+
const NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
46
|
+
function unquote(value) {
|
|
47
|
+
const trimmed = value.trim();
|
|
48
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"") || trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
|
|
49
|
+
return trimmed;
|
|
50
|
+
}
|
|
51
|
+
function parseBool(value, fallback) {
|
|
52
|
+
if (value === void 0) return fallback;
|
|
53
|
+
const normalized = unquote(value).trim().toLowerCase();
|
|
54
|
+
if ([
|
|
55
|
+
"true",
|
|
56
|
+
"yes",
|
|
57
|
+
"on",
|
|
58
|
+
"1"
|
|
59
|
+
].includes(normalized)) return true;
|
|
60
|
+
if ([
|
|
61
|
+
"false",
|
|
62
|
+
"no",
|
|
63
|
+
"off",
|
|
64
|
+
"0"
|
|
65
|
+
].includes(normalized)) return false;
|
|
66
|
+
throw new Error(`invalid boolean "${value}"`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Minimal YAML frontmatter reader for SKILL.md.
|
|
70
|
+
* Official keys: name, description, whenToUse, disable-model-invocation, user-invocable.
|
|
71
|
+
* `packages/skill/skill-filesystem/README.md`.
|
|
72
|
+
*/
|
|
73
|
+
function parseSkillMarkdown(text, fallbackName) {
|
|
74
|
+
if (!text.startsWith("---")) return void 0;
|
|
75
|
+
const end = text.indexOf("\n---", 3);
|
|
76
|
+
if (end < 0) return void 0;
|
|
77
|
+
const raw = text.slice(4, end);
|
|
78
|
+
const body = text.slice(end + 4).replace(/^\s*\n/, "");
|
|
79
|
+
const fields = {};
|
|
80
|
+
for (const line of raw.split("\n")) {
|
|
81
|
+
const match = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
|
|
82
|
+
if (!match) continue;
|
|
83
|
+
fields[match[1]] = match[2] ?? "";
|
|
84
|
+
}
|
|
85
|
+
let name = unquote(fields.name ?? fallbackName);
|
|
86
|
+
if (name === "Poteto Mode") name = "poteto-mode";
|
|
87
|
+
if (!NAME_RE.test(name)) return void 0;
|
|
88
|
+
const description = unquote(fields.description ?? "");
|
|
89
|
+
if (description.length === 0) return void 0;
|
|
90
|
+
try {
|
|
91
|
+
const disableModel = parseBool(fields["disable-model-invocation"], false);
|
|
92
|
+
const userInvocable = parseBool(fields["user-invocable"], true);
|
|
93
|
+
return {
|
|
94
|
+
frontmatter: {
|
|
95
|
+
name,
|
|
96
|
+
description,
|
|
97
|
+
...fields.whenToUse ? { whenToUse: unquote(fields.whenToUse) } : {},
|
|
98
|
+
modelInvocable: !disableModel,
|
|
99
|
+
userInvocable
|
|
100
|
+
},
|
|
101
|
+
content: body.trimStart()
|
|
102
|
+
};
|
|
103
|
+
} catch {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region src/skills-provider.ts
|
|
109
|
+
const BUNDLED_SKILL_RANK = 600;
|
|
110
|
+
function defaultSkillsRoot() {
|
|
111
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "skills");
|
|
112
|
+
}
|
|
113
|
+
async function loadSkills(root) {
|
|
114
|
+
let entries = [];
|
|
115
|
+
try {
|
|
116
|
+
entries = await readdir(root);
|
|
117
|
+
} catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
const records = [];
|
|
121
|
+
for (const name of entries) {
|
|
122
|
+
const dir = join(root, name);
|
|
123
|
+
const path = join(dir, "SKILL.md");
|
|
124
|
+
let text;
|
|
125
|
+
try {
|
|
126
|
+
text = await readFile(path, "utf8");
|
|
127
|
+
} catch {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const parsed = parseSkillMarkdown(text, name);
|
|
131
|
+
if (parsed === void 0) continue;
|
|
132
|
+
records.push({
|
|
133
|
+
name: parsed.frontmatter.name,
|
|
134
|
+
description: parsed.frontmatter.description,
|
|
135
|
+
...parsed.frontmatter.whenToUse === void 0 ? {} : { whenToUse: parsed.frontmatter.whenToUse },
|
|
136
|
+
modelInvocable: parsed.frontmatter.modelInvocable,
|
|
137
|
+
userInvocable: parsed.frontmatter.userInvocable,
|
|
138
|
+
content: parsed.content,
|
|
139
|
+
dir
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return records.sort((a, b) => a.name.localeCompare(b.name));
|
|
143
|
+
}
|
|
144
|
+
function createSkillProvider(skillsRoot = defaultSkillsRoot()) {
|
|
145
|
+
return {
|
|
146
|
+
name: SKILL_PROVIDER_NAME,
|
|
147
|
+
async list() {
|
|
148
|
+
return (await loadSkills(skillsRoot)).map((skill) => ({
|
|
149
|
+
name: skill.name,
|
|
150
|
+
description: skill.description,
|
|
151
|
+
...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
|
|
152
|
+
invocation: {
|
|
153
|
+
modelInvocable: skill.modelInvocable,
|
|
154
|
+
userInvocable: skill.userInvocable
|
|
155
|
+
},
|
|
156
|
+
provider: SKILL_PROVIDER_NAME,
|
|
157
|
+
source: "bundled",
|
|
158
|
+
resourceBase: {
|
|
159
|
+
kind: "directory",
|
|
160
|
+
path: skill.dir
|
|
161
|
+
},
|
|
162
|
+
rank: BUNDLED_SKILL_RANK,
|
|
163
|
+
locator: skill.dir,
|
|
164
|
+
path: join(skill.dir, "SKILL.md")
|
|
165
|
+
}));
|
|
166
|
+
},
|
|
167
|
+
async get(candidate) {
|
|
168
|
+
const skill = (await loadSkills(skillsRoot)).find((entry) => entry.name === candidate.name);
|
|
169
|
+
if (skill === void 0) return void 0;
|
|
170
|
+
return {
|
|
171
|
+
name: skill.name,
|
|
172
|
+
description: skill.description,
|
|
173
|
+
...skill.whenToUse === void 0 ? {} : { whenToUse: skill.whenToUse },
|
|
174
|
+
invocation: {
|
|
175
|
+
modelInvocable: skill.modelInvocable,
|
|
176
|
+
userInvocable: skill.userInvocable
|
|
177
|
+
},
|
|
178
|
+
provider: SKILL_PROVIDER_NAME,
|
|
179
|
+
source: "bundled",
|
|
180
|
+
resourceBase: {
|
|
181
|
+
kind: "directory",
|
|
182
|
+
path: skill.dir
|
|
183
|
+
},
|
|
184
|
+
content: skill.content,
|
|
185
|
+
path: join(skill.dir, "SKILL.md")
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/request-overlay.ts
|
|
192
|
+
var RoleEffortMap = class {
|
|
193
|
+
bySession = /* @__PURE__ */ new Map();
|
|
194
|
+
remember(sessionId, binding) {
|
|
195
|
+
this.bySession.set(sessionId, binding);
|
|
196
|
+
}
|
|
197
|
+
forget(sessionId) {
|
|
198
|
+
this.bySession.delete(sessionId);
|
|
199
|
+
}
|
|
200
|
+
lookup(sessionId) {
|
|
201
|
+
return this.bySession.get(sessionId);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/home.ts
|
|
206
|
+
/** Official precedence: `$DSH_HOME`, then `~/.dsh`. `packages/util/home-paths/README.md`. */
|
|
207
|
+
function resolveDshHome(env = process.env) {
|
|
208
|
+
const fromEnv = env.DSH_HOME?.trim();
|
|
209
|
+
if (fromEnv) return resolve(fromEnv);
|
|
210
|
+
return join(homedir(), ".dsh");
|
|
211
|
+
}
|
|
212
|
+
//#endregion
|
|
213
|
+
//#region src/catalog-types.ts
|
|
214
|
+
/** Hints for known logins whose adapter is missing; not an allowlist for live adapters. */
|
|
215
|
+
const OAUTH_ROUTE_BY_STORE_ID = {
|
|
216
|
+
"openai-codex": "pi-openai-codex",
|
|
217
|
+
anthropic: "pi-anthropic",
|
|
218
|
+
xai: "pi-xai",
|
|
219
|
+
"github-copilot": "pi-github-copilot",
|
|
220
|
+
openrouter: "pi-openrouter",
|
|
221
|
+
"kimi-coding": "pi-kimi-coding",
|
|
222
|
+
antigravity: "agy-google-antigravity"
|
|
223
|
+
};
|
|
224
|
+
function storeIdForRoute(route) {
|
|
225
|
+
if (route.startsWith("pi-") && route.length > 3) return route.slice(3);
|
|
226
|
+
if (route === "agy-google-antigravity" || route.startsWith("agy-")) return "antigravity";
|
|
227
|
+
}
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/roles.ts
|
|
230
|
+
/**
|
|
231
|
+
* pstack role keys used by playbooks and Settings → pstack.
|
|
232
|
+
* These are pstack names, not Cursor slugs and not grok-build `subagent_type` values.
|
|
233
|
+
*/
|
|
234
|
+
const SCALAR_ROLES = [
|
|
235
|
+
"feature",
|
|
236
|
+
"refactoring",
|
|
237
|
+
"bug-fix",
|
|
238
|
+
"perf-issue",
|
|
239
|
+
"hillclimb",
|
|
240
|
+
"judgment-and-prose",
|
|
241
|
+
"hardest-tasks",
|
|
242
|
+
"how-explorer",
|
|
243
|
+
"how-explainer",
|
|
244
|
+
"why-investigators",
|
|
245
|
+
"why-synthesizer",
|
|
246
|
+
"reflect-tooling",
|
|
247
|
+
"reflect-judgment",
|
|
248
|
+
"swarm-workers",
|
|
249
|
+
"independent-verifier",
|
|
250
|
+
"poteto-agent",
|
|
251
|
+
"comment-sicko"
|
|
252
|
+
];
|
|
253
|
+
const PANEL_ROLES = [
|
|
254
|
+
"how-critics",
|
|
255
|
+
"arena-runners",
|
|
256
|
+
"arena-cross-judge-pool",
|
|
257
|
+
"architect-runners",
|
|
258
|
+
"interrogate-reviewers"
|
|
259
|
+
];
|
|
260
|
+
const ALL_ROLES = [...SCALAR_ROLES, ...PANEL_ROLES];
|
|
261
|
+
function isPstackRole(value) {
|
|
262
|
+
return ALL_ROLES.includes(value);
|
|
263
|
+
}
|
|
264
|
+
function isPanelRole(value) {
|
|
265
|
+
return PANEL_ROLES.includes(value);
|
|
266
|
+
}
|
|
267
|
+
/** Alias accepted from Cursor-era skills. */
|
|
268
|
+
function normalizeRole(raw) {
|
|
269
|
+
const trimmed = raw.trim();
|
|
270
|
+
if (trimmed === "Comment Sicko" || trimmed === "comment sicko") return "comment-sicko";
|
|
271
|
+
if (trimmed === "poteto-agent" || trimmed === "Poteto Agent") return "poteto-agent";
|
|
272
|
+
if (trimmed === "generalPurpose" || trimmed === "general-purpose") return "poteto-agent";
|
|
273
|
+
return trimmed;
|
|
274
|
+
}
|
|
275
|
+
function emptyOverlay() {
|
|
276
|
+
const roles = {};
|
|
277
|
+
for (const role of ALL_ROLES) roles[role] = {
|
|
278
|
+
inherit: true,
|
|
279
|
+
routes: []
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
version: 1,
|
|
283
|
+
roles
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function routeKey(provider, model) {
|
|
287
|
+
return `${provider}::${model}`;
|
|
288
|
+
}
|
|
289
|
+
function isRecord$2(value) {
|
|
290
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
291
|
+
}
|
|
292
|
+
function parseRoute(raw, path) {
|
|
293
|
+
if (!isRecord$2(raw)) throw new Error(`${path} must be an object`);
|
|
294
|
+
const provider = raw.provider;
|
|
295
|
+
const model = raw.model;
|
|
296
|
+
if (typeof provider !== "string" || provider.length === 0) throw new Error(`${path}.provider must be a non-empty string`);
|
|
297
|
+
if (typeof model !== "string" || model.length === 0) throw new Error(`${path}.model must be a non-empty string`);
|
|
298
|
+
const effort = raw.reasoningEffort;
|
|
299
|
+
if (effort !== void 0 && (typeof effort !== "string" || effort.length === 0)) throw new Error(`${path}.reasoningEffort must be a non-empty string when set`);
|
|
300
|
+
const extra = Object.keys(raw).filter((key) => key !== "provider" && key !== "model" && key !== "reasoningEffort");
|
|
301
|
+
if (extra.length > 0) throw new Error(`${path} has unknown fields: ${extra.join(", ")}`);
|
|
302
|
+
return effort === void 0 ? {
|
|
303
|
+
provider,
|
|
304
|
+
model
|
|
305
|
+
} : {
|
|
306
|
+
provider,
|
|
307
|
+
model,
|
|
308
|
+
reasoningEffort: effort
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function parseRole(raw, role) {
|
|
312
|
+
if (!isRecord$2(raw)) throw new Error(`roles.${role} must be an object`);
|
|
313
|
+
const extra = Object.keys(raw).filter((key) => key !== "inherit" && key !== "routes");
|
|
314
|
+
if (extra.length > 0) throw new Error(`roles.${role} has unknown fields: ${extra.join(", ")}`);
|
|
315
|
+
const inherit = raw.inherit === true || raw.inherit === "true" || raw.inherit === "inherit-parent" || raw.inherit === "auto";
|
|
316
|
+
const routesRaw = raw.routes;
|
|
317
|
+
const routes = Array.isArray(routesRaw) ? routesRaw.map((entry, index) => parseRoute(entry, `roles.${role}.routes[${index}]`)) : [];
|
|
318
|
+
if (inherit) return {
|
|
319
|
+
inherit: true,
|
|
320
|
+
routes: []
|
|
321
|
+
};
|
|
322
|
+
if (routes.length === 0) return {
|
|
323
|
+
inherit: true,
|
|
324
|
+
routes: []
|
|
325
|
+
};
|
|
326
|
+
if (!isPanelRole(role) && routes.length !== 1) throw new Error(`roles.${role} is a scalar role and may have at most one route`);
|
|
327
|
+
return {
|
|
328
|
+
inherit: false,
|
|
329
|
+
routes
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function parseOverlay(text) {
|
|
333
|
+
let value;
|
|
334
|
+
try {
|
|
335
|
+
value = JSON.parse(text);
|
|
336
|
+
} catch {
|
|
337
|
+
throw new Error("pstack-dsh overlay is not valid JSON");
|
|
338
|
+
}
|
|
339
|
+
if (!isRecord$2(value)) throw new Error("pstack-dsh overlay must be an object");
|
|
340
|
+
if (value.version !== 1) throw new Error(`pstack-dsh overlay version ${String(value.version)} is unsupported`);
|
|
341
|
+
const extra = Object.keys(value).filter((key) => key !== "version" && key !== "roles");
|
|
342
|
+
if (extra.length > 0) throw new Error(`pstack-dsh overlay has unknown fields: ${extra.join(", ")}`);
|
|
343
|
+
const rolesRaw = value.roles;
|
|
344
|
+
if (!isRecord$2(rolesRaw)) throw new Error("pstack-dsh overlay.roles must be an object");
|
|
345
|
+
const roles = {};
|
|
346
|
+
for (const role of ALL_ROLES) {
|
|
347
|
+
const entry = rolesRaw[role];
|
|
348
|
+
roles[role] = entry === void 0 ? {
|
|
349
|
+
inherit: true,
|
|
350
|
+
routes: []
|
|
351
|
+
} : parseRole(entry, role);
|
|
352
|
+
}
|
|
353
|
+
for (const key of Object.keys(rolesRaw)) if (!isPstackRole(key)) throw new Error(`unknown pstack role "${key}"`);
|
|
354
|
+
return {
|
|
355
|
+
version: 1,
|
|
356
|
+
roles
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function validateOverlayAgainstCatalog(overlay, routes) {
|
|
360
|
+
const errors = [];
|
|
361
|
+
const live = new Map(routes.filter((route) => route.selectable).map((route) => [routeKey(route.provider, route.model), route]));
|
|
362
|
+
for (const role of ALL_ROLES) {
|
|
363
|
+
if (role === "how-critics") continue;
|
|
364
|
+
const assignment = overlay.roles[role] ?? {
|
|
365
|
+
inherit: true,
|
|
366
|
+
routes: []
|
|
367
|
+
};
|
|
368
|
+
if (assignment.inherit) continue;
|
|
369
|
+
for (const [index, entry] of assignment.routes.entries()) {
|
|
370
|
+
const found = live.get(routeKey(entry.provider, entry.model));
|
|
371
|
+
if (found === void 0) {
|
|
372
|
+
errors.push(`${role}[${index}]: ${entry.provider}/${entry.model} is not a logged-in live route`);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (entry.reasoningEffort === void 0) continue;
|
|
376
|
+
if (found.efforts.length === 0) {
|
|
377
|
+
errors.push(`${role}[${index}]: ${entry.provider}/${entry.model} has no effort field; omit reasoningEffort`);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (!found.efforts.some((effort) => effort.id === entry.reasoningEffort)) errors.push(`${role}[${index}]: effort "${entry.reasoningEffort}" is not accepted by ${entry.provider}/${entry.model} (live: ${found.efforts.map((effort) => effort.id).join(", ")})`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return errors;
|
|
384
|
+
}
|
|
385
|
+
function resolveRole(overlay, role) {
|
|
386
|
+
const key = role;
|
|
387
|
+
return overlay.roles[key] ?? {
|
|
388
|
+
inherit: true,
|
|
389
|
+
routes: []
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/overlay.ts
|
|
394
|
+
function overlayPath(dshHome = resolveDshHome()) {
|
|
395
|
+
return join(dshHome, OVERLAY_FILENAME);
|
|
396
|
+
}
|
|
397
|
+
async function readOverlay(dshHome = resolveDshHome()) {
|
|
398
|
+
const path = overlayPath(dshHome);
|
|
399
|
+
try {
|
|
400
|
+
return {
|
|
401
|
+
path,
|
|
402
|
+
overlay: parseOverlay(await readFile(path, "utf8")),
|
|
403
|
+
missing: false
|
|
404
|
+
};
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (error.code === "ENOENT") return {
|
|
407
|
+
path,
|
|
408
|
+
overlay: emptyOverlay(),
|
|
409
|
+
missing: true
|
|
410
|
+
};
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
async function writeOverlay(overlay, dshHome = resolveDshHome()) {
|
|
415
|
+
const path = overlayPath(dshHome);
|
|
416
|
+
const body = `${JSON.stringify(overlay, null, 2)}\n`;
|
|
417
|
+
await writeFileAtomic(path, body, {
|
|
418
|
+
mode: 384,
|
|
419
|
+
dirMode: 448
|
|
420
|
+
});
|
|
421
|
+
return path;
|
|
422
|
+
}
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/catalog.ts
|
|
425
|
+
const API_KEY_REF = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
426
|
+
function isRecord$1(value) {
|
|
427
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
428
|
+
}
|
|
429
|
+
function collectApiKeyEnvRefs(value, out) {
|
|
430
|
+
if (Array.isArray(value)) {
|
|
431
|
+
for (const item of value) collectApiKeyEnvRefs(item, out);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (!isRecord$1(value)) return;
|
|
435
|
+
for (const [key, child] of Object.entries(value)) if (key === "apiKeyEnv" && typeof child === "string" && API_KEY_REF.test(child)) out.add(child);
|
|
436
|
+
else collectApiKeyEnvRefs(child, out);
|
|
437
|
+
}
|
|
438
|
+
function sliceAtPath(value, path) {
|
|
439
|
+
let current = value;
|
|
440
|
+
for (const segment of path) {
|
|
441
|
+
if (!isRecord$1(current)) return void 0;
|
|
442
|
+
current = current[segment];
|
|
443
|
+
}
|
|
444
|
+
return current;
|
|
445
|
+
}
|
|
446
|
+
function settingsValue(entry) {
|
|
447
|
+
if (!isRecord$1(entry)) return void 0;
|
|
448
|
+
return entry.value ?? entry.base ?? entry.user;
|
|
449
|
+
}
|
|
450
|
+
function settingsNs(entry) {
|
|
451
|
+
if (!isRecord$1(entry)) return void 0;
|
|
452
|
+
return typeof entry.ns === "string" ? entry.ns : void 0;
|
|
453
|
+
}
|
|
454
|
+
async function refConfigured(credentials, env, ref) {
|
|
455
|
+
if (credentials !== void 0) try {
|
|
456
|
+
return (await credentials.describe(ref)).configured === true;
|
|
457
|
+
} catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
const ambient = env[ref];
|
|
461
|
+
return typeof ambient === "string" && ambient.trim().length > 0;
|
|
462
|
+
}
|
|
463
|
+
async function readOauthStoreIds(dshHome) {
|
|
464
|
+
const candidates = [join(dshHome, OAUTH_AUTH_FILENAME), join(dshHome, OAUTH_AUTH_LEGACY_FILENAME)];
|
|
465
|
+
let present = false;
|
|
466
|
+
const ids = /* @__PURE__ */ new Set();
|
|
467
|
+
for (const filename of candidates) try {
|
|
468
|
+
const text = await readFile(filename, "utf8");
|
|
469
|
+
const parsed = JSON.parse(text);
|
|
470
|
+
if (!isRecord$1(parsed) || !isRecord$1(parsed.credentials)) continue;
|
|
471
|
+
present = true;
|
|
472
|
+
for (const key of Object.keys(parsed.credentials)) ids.add(key);
|
|
473
|
+
break;
|
|
474
|
+
} catch (error) {
|
|
475
|
+
if (error.code === "ENOENT") continue;
|
|
476
|
+
present = true;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const text = await readFile(join(dshHome, ANTIGRAVITY_AUTH_FILENAME), "utf8");
|
|
481
|
+
const parsed = JSON.parse(text);
|
|
482
|
+
if (isRecord$1(parsed) && isRecord$1(parsed.credential)) {
|
|
483
|
+
present = true;
|
|
484
|
+
const cred = parsed.credential;
|
|
485
|
+
if (cred.type === "oauth" && typeof cred.projectId === "string" && cred.projectId.trim().length > 0 && typeof cred.expires === "number" && cred.expires > 0) ids.add("antigravity");
|
|
486
|
+
}
|
|
487
|
+
} catch (error) {
|
|
488
|
+
if (error.code !== "ENOENT") present = true;
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
present,
|
|
492
|
+
ids: [...ids]
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
function effortsOf(info) {
|
|
496
|
+
const reasoning = info.reasoning;
|
|
497
|
+
if (reasoning === void 0 || !Array.isArray(reasoning.efforts)) return { efforts: [] };
|
|
498
|
+
return {
|
|
499
|
+
efforts: reasoning.efforts.map((effort) => ({
|
|
500
|
+
id: effort.id,
|
|
501
|
+
name: effort.name,
|
|
502
|
+
...effort.description === void 0 ? {} : { description: effort.description }
|
|
503
|
+
})),
|
|
504
|
+
...reasoning.defaultEffort === void 0 ? {} : { defaultEffort: reasoning.defaultEffort }
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Live logged-in routes only. No vendor catalog. No invented slugs.
|
|
509
|
+
*/
|
|
510
|
+
async function buildCatalog(host, signal) {
|
|
511
|
+
const dshHome = host.dshHome ?? resolveDshHome(host.env);
|
|
512
|
+
const env = host.env ?? process.env;
|
|
513
|
+
const overlay = await readOverlay(dshHome);
|
|
514
|
+
const oauth = await readOauthStoreIds(dshHome);
|
|
515
|
+
const llm = host.llm;
|
|
516
|
+
const providers = llm?.listProviders() ?? [];
|
|
517
|
+
const registered = new Set(providers.map((provider) => provider.id));
|
|
518
|
+
const oauthPluginPresent = [...registered].some((id) => id.startsWith("pi-") || id === "agy-google-antigravity" || id.startsWith("agy-"));
|
|
519
|
+
const recommendOauthLogin = oauth.ids.length === 0 && !oauthPluginPresent;
|
|
520
|
+
const refsByProvider = /* @__PURE__ */ new Map();
|
|
521
|
+
const remember = (provider, ref) => {
|
|
522
|
+
const set = refsByProvider.get(provider) ?? /* @__PURE__ */ new Set();
|
|
523
|
+
set.add(ref);
|
|
524
|
+
refsByProvider.set(provider, set);
|
|
525
|
+
};
|
|
526
|
+
if (host.settings !== void 0) {
|
|
527
|
+
let described = [];
|
|
528
|
+
try {
|
|
529
|
+
described = host.settings.describe({ redactSecrets: true });
|
|
530
|
+
} catch {
|
|
531
|
+
described = [];
|
|
532
|
+
}
|
|
533
|
+
const configurable = llm?.listConfigurableProviders?.() ?? [];
|
|
534
|
+
for (const entry of described) {
|
|
535
|
+
const ns = settingsNs(entry);
|
|
536
|
+
const value = settingsValue(entry);
|
|
537
|
+
for (const row of configurable) {
|
|
538
|
+
if (ns !== void 0 && row.settingsNs !== ns) continue;
|
|
539
|
+
const slice = row.settingsPath.length === 0 ? value : sliceAtPath(value, row.settingsPath);
|
|
540
|
+
const refs = /* @__PURE__ */ new Set();
|
|
541
|
+
collectApiKeyEnvRefs(slice ?? value, refs);
|
|
542
|
+
for (const ref of refs) remember(row.provider, ref);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
for (const provider of providers) if (provider.id === "deepseek-official") remember(provider.id, DEEPSEEK_API_KEY_ENV);
|
|
547
|
+
const routes = [];
|
|
548
|
+
const seen = /* @__PURE__ */ new Set();
|
|
549
|
+
const push = (route) => {
|
|
550
|
+
const key = `${route.provider}::${route.model}`;
|
|
551
|
+
if (seen.has(key)) return;
|
|
552
|
+
seen.add(key);
|
|
553
|
+
routes.push(route);
|
|
554
|
+
};
|
|
555
|
+
if (llm !== void 0) for (const provider of providers) {
|
|
556
|
+
const storeId = storeIdForRoute(provider.id);
|
|
557
|
+
const oauthSignedIn = storeId !== void 0 && oauth.ids.includes(storeId);
|
|
558
|
+
let models = [];
|
|
559
|
+
try {
|
|
560
|
+
models = await llm.listModels(provider.id);
|
|
561
|
+
} catch {
|
|
562
|
+
models = [];
|
|
563
|
+
}
|
|
564
|
+
if (models.length === 0) continue;
|
|
565
|
+
const refs = [...refsByProvider.get(provider.id) ?? []];
|
|
566
|
+
let keyPresent = false;
|
|
567
|
+
for (const ref of refs) if (await refConfigured(host.credentials, env, ref)) {
|
|
568
|
+
keyPresent = true;
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
const selectable = oauthSignedIn || keyPresent;
|
|
572
|
+
const source = oauthSignedIn ? "oauth" : "api-key";
|
|
573
|
+
if (!selectable) continue;
|
|
574
|
+
for (const model of models) {
|
|
575
|
+
let resolved;
|
|
576
|
+
try {
|
|
577
|
+
resolved = await llm.resolveModelInfo(provider.id, model.id, signal);
|
|
578
|
+
} catch {
|
|
579
|
+
resolved = {
|
|
580
|
+
id: model.id,
|
|
581
|
+
name: model.name
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
const { efforts, defaultEffort } = effortsOf(resolved);
|
|
585
|
+
push({
|
|
586
|
+
provider: provider.id,
|
|
587
|
+
providerName: provider.name,
|
|
588
|
+
model: model.id,
|
|
589
|
+
modelName: resolved.name || model.name,
|
|
590
|
+
selectable: true,
|
|
591
|
+
source,
|
|
592
|
+
...oauthSignedIn ? { oauthSignedIn: true } : {},
|
|
593
|
+
routeRegistered: true,
|
|
594
|
+
efforts,
|
|
595
|
+
...defaultEffort === void 0 ? {} : { defaultEffort }
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
for (const id of oauth.ids) {
|
|
600
|
+
const route = OAUTH_ROUTE_BY_STORE_ID[id];
|
|
601
|
+
if (route === void 0) continue;
|
|
602
|
+
if (registered.has(route)) continue;
|
|
603
|
+
const isAntigravity = id === "antigravity" || route === "agy-google-antigravity";
|
|
604
|
+
push({
|
|
605
|
+
provider: route,
|
|
606
|
+
providerName: isAntigravity ? "Google Antigravity" : id,
|
|
607
|
+
model: "*",
|
|
608
|
+
modelName: isAntigravity ? "(install dsh-antigravity-oauth to load this route)" : "(install dsh-oauth-login to load this route)",
|
|
609
|
+
selectable: false,
|
|
610
|
+
source: "oauth",
|
|
611
|
+
oauthSignedIn: true,
|
|
612
|
+
routeRegistered: false,
|
|
613
|
+
efforts: [],
|
|
614
|
+
hint: isAntigravity ? "Signed in at $DSH_HOME/.dsh-antigravity-oauth.json. Install dsh-antigravity-oauth so the route is live." : "Signed in at $DSH_HOME/.dsh-oauth-auth.json. Install https://github.com/aa2246740/dsh-oauth-login so the route is live."
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
const selectableCount = routes.filter((route) => route.selectable).length;
|
|
618
|
+
return {
|
|
619
|
+
routes,
|
|
620
|
+
selectableCount,
|
|
621
|
+
oauthPluginPresent,
|
|
622
|
+
oauthStorePresent: oauth.present,
|
|
623
|
+
oauthSignedInProviders: oauth.ids,
|
|
624
|
+
overlayPath: overlay.path,
|
|
625
|
+
overlayMissing: overlay.missing,
|
|
626
|
+
inheritParent: true,
|
|
627
|
+
recommendOauthLogin,
|
|
628
|
+
...selectableCount === 0 ? { emptyReason: recommendOauthLogin ? "No logged-in API key and no dsh-oauth-login or dsh-antigravity-oauth store. Children inherit this conversation. Add a key in DSH, or install/login https://github.com/aa2246740/dsh-oauth-login / dsh-antigravity-oauth." : "No selectable live route. Children inherit this conversation." } : {}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
//#endregion
|
|
632
|
+
//#region src/persona.ts
|
|
633
|
+
function defaultAgentsRoot() {
|
|
634
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "agents");
|
|
635
|
+
}
|
|
636
|
+
function stripFrontmatter(text) {
|
|
637
|
+
if (!text.startsWith("---")) return text;
|
|
638
|
+
const end = text.indexOf("\n---", 3);
|
|
639
|
+
if (end < 0) return text;
|
|
640
|
+
return text.slice(end + 4).replace(/^\s*\n/, "");
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Persona for a pstack role. Official spawn persona shadows deployment persona
|
|
644
|
+
* (`SubagentStartRequest.persona`, `packages/subagent/subagent/src/types.ts`).
|
|
645
|
+
*/
|
|
646
|
+
async function loadPersona(role, agentsRoot = defaultAgentsRoot()) {
|
|
647
|
+
const key = normalizeRole(role);
|
|
648
|
+
const files = key === "comment-sicko" ? ["comment-sicko.md"] : key === "poteto-agent" ? ["poteto-agent.md"] : [`${key}.md`, "poteto-agent.md"];
|
|
649
|
+
for (const file of files) try {
|
|
650
|
+
const body = stripFrontmatter(await readFile(join(agentsRoot, file), "utf8")).trim();
|
|
651
|
+
if (body.length > 0) return body;
|
|
652
|
+
} catch {
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
//#endregion
|
|
657
|
+
//#region src/spawn.ts
|
|
658
|
+
function resolveSpawn(overlay, request) {
|
|
659
|
+
const role = normalizeRole(request.role);
|
|
660
|
+
const assignment = resolveRole(overlay, role);
|
|
661
|
+
const inherit = assignment.inherit || assignment.routes.length === 0;
|
|
662
|
+
const index = request.routeIndex ?? 0;
|
|
663
|
+
const route = inherit ? void 0 : assignment.routes[index] ?? assignment.routes[0];
|
|
664
|
+
return {
|
|
665
|
+
role,
|
|
666
|
+
description: request.description,
|
|
667
|
+
prompt: request.prompt,
|
|
668
|
+
runInBackground: request.runInBackground,
|
|
669
|
+
inherit,
|
|
670
|
+
...route === void 0 ? {} : {
|
|
671
|
+
agentOptions: {
|
|
672
|
+
provider: route.provider,
|
|
673
|
+
model: route.model
|
|
674
|
+
},
|
|
675
|
+
route,
|
|
676
|
+
...route.reasoningEffort === void 0 ? {} : { reasoningEffort: route.reasoningEffort }
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region src/tools.ts
|
|
682
|
+
function textPrompt(prompt) {
|
|
683
|
+
return [{
|
|
684
|
+
type: "text",
|
|
685
|
+
text: prompt
|
|
686
|
+
}];
|
|
687
|
+
}
|
|
688
|
+
function catalogTool(host) {
|
|
689
|
+
return {
|
|
690
|
+
name: TOOL_CATALOG,
|
|
691
|
+
description: "List live DSH LLM routes pstack may use: registered adapters with a logged-in API key, plus OAuth store routes (dsh-oauth-login, dsh-antigravity-oauth) that are already signed in. Never a vendor catalog. Secrets are omitted.",
|
|
692
|
+
parameters: {},
|
|
693
|
+
async execute(_args, exec) {
|
|
694
|
+
return buildCatalog({
|
|
695
|
+
llm: host.llm,
|
|
696
|
+
credentials: host.credentials,
|
|
697
|
+
settings: host.settings,
|
|
698
|
+
dshHome: host.dshHome,
|
|
699
|
+
env: host.env
|
|
700
|
+
}, exec.signal);
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function overlayReadTool(host) {
|
|
705
|
+
return {
|
|
706
|
+
name: TOOL_OVERLAY_READ,
|
|
707
|
+
description: "Read $DSH_HOME/pstack-dsh.json. Missing file means inherit the parent conversation for every role.",
|
|
708
|
+
parameters: {},
|
|
709
|
+
async execute() {
|
|
710
|
+
const current = await readOverlay(host.dshHome ?? resolveDshHome(host.env));
|
|
711
|
+
return {
|
|
712
|
+
path: current.path,
|
|
713
|
+
missing: current.missing,
|
|
714
|
+
overlay: current.overlay
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function overlayWriteTool(host) {
|
|
720
|
+
return {
|
|
721
|
+
name: TOOL_OVERLAY_WRITE,
|
|
722
|
+
description: "Write $DSH_HOME/pstack-dsh.json after validating every provider/model/effort against pstack_catalog. inherit-parent is stored as inherit: true with empty routes.",
|
|
723
|
+
parameters: { overlay: {
|
|
724
|
+
type: "json",
|
|
725
|
+
required: true,
|
|
726
|
+
description: "Full overlay object { version: 1, roles: { <role>: { inherit, routes } } }"
|
|
727
|
+
} },
|
|
728
|
+
async execute(args) {
|
|
729
|
+
const overlay = typeof args.overlay === "string" ? parseOverlay(args.overlay) : parseOverlay(JSON.stringify(args.overlay));
|
|
730
|
+
const errors = validateOverlayAgainstCatalog(overlay, (await buildCatalog({
|
|
731
|
+
llm: host.llm,
|
|
732
|
+
credentials: host.credentials,
|
|
733
|
+
settings: host.settings,
|
|
734
|
+
dshHome: host.dshHome,
|
|
735
|
+
env: host.env
|
|
736
|
+
})).routes);
|
|
737
|
+
if (errors.length > 0) throw new Error(`overlay rejected against live catalog:\n${errors.join("\n")}`);
|
|
738
|
+
return {
|
|
739
|
+
path: await writeOverlay(overlay, host.dshHome ?? resolveDshHome(host.env)),
|
|
740
|
+
overlay
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
function spawnTool(host) {
|
|
746
|
+
return {
|
|
747
|
+
name: TOOL_SPAWN,
|
|
748
|
+
description: "Delegate a pstack role to a DSH subagent via ctx.subagents (spawn provider). Send description, prompt, role, and optional run_in_background. Do not send model, provider, reasoning_effort, thinking, isolation, or subagent_type. Route and effort come from $DSH_HOME/pstack-dsh.json when that role is mapped to a live logged-in route; otherwise the child inherits this conversation.",
|
|
749
|
+
parameters: {
|
|
750
|
+
description: {
|
|
751
|
+
type: "string",
|
|
752
|
+
required: true,
|
|
753
|
+
description: "A short (3-5 word) description of the delegated task, for display."
|
|
754
|
+
},
|
|
755
|
+
prompt: {
|
|
756
|
+
type: "string",
|
|
757
|
+
required: true,
|
|
758
|
+
description: "The complete, self-contained task. The child does not see this conversation."
|
|
759
|
+
},
|
|
760
|
+
role: {
|
|
761
|
+
type: "string",
|
|
762
|
+
required: true,
|
|
763
|
+
description: "pstack role key (feature, how-explainer, poteto-agent, comment-sicko, …)."
|
|
764
|
+
},
|
|
765
|
+
run_in_background: {
|
|
766
|
+
type: "boolean",
|
|
767
|
+
description: "Default true. Continuable children return a subagent id; one-shot background returns a job-like start without waiting."
|
|
768
|
+
},
|
|
769
|
+
route_index: {
|
|
770
|
+
type: "number",
|
|
771
|
+
description: "Which overlay route to use for a panel role. Default 0."
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
async execute(args, exec) {
|
|
775
|
+
const parent = exec.agent;
|
|
776
|
+
if (parent === void 0) throw new Error("pstack_spawn requires a calling agent");
|
|
777
|
+
const subagents = host.subagents;
|
|
778
|
+
if (subagents === void 0) throw new Error("pstack_spawn requires ctx.subagents");
|
|
779
|
+
const role = normalizeRole(String(args.role ?? ""));
|
|
780
|
+
if (role === "how-critics") return {
|
|
781
|
+
kind: "disabled",
|
|
782
|
+
role,
|
|
783
|
+
reason: "how-critics was retired in pstack 0.15; no agent was started."
|
|
784
|
+
};
|
|
785
|
+
if (!isPstackRole(role) && role.length === 0) throw new Error("pstack_spawn role is required");
|
|
786
|
+
const overlay = (await readOverlay(host.dshHome ?? resolveDshHome(host.env))).overlay;
|
|
787
|
+
const resolved = resolveSpawn(overlay, {
|
|
788
|
+
role,
|
|
789
|
+
description: String(args.description ?? ""),
|
|
790
|
+
prompt: String(args.prompt ?? ""),
|
|
791
|
+
runInBackground: args.run_in_background !== false,
|
|
792
|
+
routeIndex: typeof args.route_index === "number" ? args.route_index : void 0
|
|
793
|
+
});
|
|
794
|
+
if (resolved.description.trim().length === 0) throw new Error("description is required");
|
|
795
|
+
if (resolved.prompt.trim().length === 0) throw new Error("prompt is required");
|
|
796
|
+
const persona = await loadPersona(resolved.role);
|
|
797
|
+
const providerName = host.spawnProvider ?? "spawn";
|
|
798
|
+
const provider = subagents.getProvider?.(providerName);
|
|
799
|
+
const request = {
|
|
800
|
+
label: resolved.description,
|
|
801
|
+
prompt: textPrompt(resolved.prompt),
|
|
802
|
+
parent,
|
|
803
|
+
signal: exec.signal,
|
|
804
|
+
...resolved.agentOptions ? { agentOptions: resolved.agentOptions } : {},
|
|
805
|
+
...persona && provider?.capabilities?.persona !== false ? { persona } : {}
|
|
806
|
+
};
|
|
807
|
+
const remember = (sessionId) => {
|
|
808
|
+
host.roles.remember(sessionId, {
|
|
809
|
+
role: resolved.role,
|
|
810
|
+
...resolved.reasoningEffort ? { reasoningEffort: resolved.reasoningEffort } : {},
|
|
811
|
+
...resolved.agentOptions?.provider ? { provider: resolved.agentOptions.provider } : {},
|
|
812
|
+
...resolved.agentOptions?.model ? { model: resolved.agentOptions.model } : {}
|
|
813
|
+
});
|
|
814
|
+
};
|
|
815
|
+
if (resolved.runInBackground && typeof subagents.startContinuable === "function" && provider?.prepareContinuable !== void 0) {
|
|
816
|
+
const started = await subagents.startContinuable({
|
|
817
|
+
provider: providerName,
|
|
818
|
+
label: resolved.description,
|
|
819
|
+
request,
|
|
820
|
+
signal: exec.signal
|
|
821
|
+
});
|
|
822
|
+
remember(started.childId);
|
|
823
|
+
return {
|
|
824
|
+
kind: "continuable",
|
|
825
|
+
subagentId: started.childId,
|
|
826
|
+
role: resolved.role,
|
|
827
|
+
inherit: resolved.inherit
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
const run = await subagents.start(providerName, request);
|
|
831
|
+
remember(run.id);
|
|
832
|
+
if (resolved.runInBackground) return {
|
|
833
|
+
kind: "foreground-detached",
|
|
834
|
+
runId: run.id,
|
|
835
|
+
role: resolved.role,
|
|
836
|
+
inherit: resolved.inherit
|
|
837
|
+
};
|
|
838
|
+
const result = await run.result;
|
|
839
|
+
await Promise.resolve(run.dispose?.());
|
|
840
|
+
return {
|
|
841
|
+
kind: "foreground",
|
|
842
|
+
runId: run.id,
|
|
843
|
+
role: resolved.role,
|
|
844
|
+
inherit: resolved.inherit,
|
|
845
|
+
output: result.output ?? null,
|
|
846
|
+
stopReason: result.stopReason ?? "completed"
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
//#endregion
|
|
852
|
+
//#region src/http-trust.ts
|
|
853
|
+
/** Loopback + same-origin. Copied shape from dsh-oauth-login `src/auth-routes.ts`. */
|
|
854
|
+
function trustedRequest(req) {
|
|
855
|
+
const remote = req.socket.remoteAddress;
|
|
856
|
+
if (remote !== "127.0.0.1" && remote !== "::1" && remote !== "::ffff:127.0.0.1") return false;
|
|
857
|
+
if (req.headers["sec-fetch-site"] === "cross-site") return false;
|
|
858
|
+
const host = req.headers.host;
|
|
859
|
+
if (host === void 0) return false;
|
|
860
|
+
const origin = req.headers.origin;
|
|
861
|
+
if (origin === void 0) return true;
|
|
862
|
+
try {
|
|
863
|
+
return new URL(origin).host === new URL(`http://${host}`).host;
|
|
864
|
+
} catch {
|
|
865
|
+
return false;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function json(res, status, value) {
|
|
869
|
+
res.writeHead(status, {
|
|
870
|
+
"content-type": "application/json; charset=utf-8",
|
|
871
|
+
"cache-control": "no-store",
|
|
872
|
+
"x-content-type-options": "nosniff"
|
|
873
|
+
});
|
|
874
|
+
res.end(JSON.stringify(value));
|
|
875
|
+
}
|
|
876
|
+
async function readJson(req, maxBytes = 262144) {
|
|
877
|
+
const chunks = [];
|
|
878
|
+
let size = 0;
|
|
879
|
+
for await (const chunk of req) {
|
|
880
|
+
size += chunk.length;
|
|
881
|
+
if (size > maxBytes) throw new Error("request body too large");
|
|
882
|
+
chunks.push(chunk);
|
|
883
|
+
}
|
|
884
|
+
if (chunks.length === 0) return {};
|
|
885
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
886
|
+
}
|
|
887
|
+
//#endregion
|
|
888
|
+
//#region src/settings-draft.ts
|
|
889
|
+
function liveFor(routes, provider, model) {
|
|
890
|
+
return routes.find((route) => route.selectable && route.provider === provider && route.model === model);
|
|
891
|
+
}
|
|
892
|
+
function stripIllegalEffort(route, live) {
|
|
893
|
+
const found = liveFor(live, route.provider, route.model);
|
|
894
|
+
if (found === void 0) return {
|
|
895
|
+
provider: route.provider,
|
|
896
|
+
model: route.model
|
|
897
|
+
};
|
|
898
|
+
if (route.reasoningEffort === void 0) return {
|
|
899
|
+
provider: route.provider,
|
|
900
|
+
model: route.model
|
|
901
|
+
};
|
|
902
|
+
if (found.efforts.length === 0) return {
|
|
903
|
+
provider: route.provider,
|
|
904
|
+
model: route.model
|
|
905
|
+
};
|
|
906
|
+
if (!found.efforts.some((effort) => effort.id === route.reasoningEffort)) return {
|
|
907
|
+
provider: route.provider,
|
|
908
|
+
model: route.model
|
|
909
|
+
};
|
|
910
|
+
return {
|
|
911
|
+
provider: route.provider,
|
|
912
|
+
model: route.model,
|
|
913
|
+
reasoningEffort: route.reasoningEffort
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
/** Stale overlay entries become inherit in the returned overlay; original file is not written. */
|
|
917
|
+
function dropUnselectableRoles(overlay, live) {
|
|
918
|
+
const next = emptyOverlay();
|
|
919
|
+
const droppedRoles = [];
|
|
920
|
+
const selectable = new Set(live.filter((route) => route.selectable).map((route) => routeKey(route.provider, route.model)));
|
|
921
|
+
for (const role of ALL_ROLES) {
|
|
922
|
+
const assignment = overlay.roles[role] ?? {
|
|
923
|
+
inherit: true,
|
|
924
|
+
routes: []
|
|
925
|
+
};
|
|
926
|
+
if (role === "how-critics") {
|
|
927
|
+
next.roles[role] = {
|
|
928
|
+
inherit: assignment.inherit,
|
|
929
|
+
routes: assignment.routes.map((route) => ({ ...route }))
|
|
930
|
+
};
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
if (assignment.inherit || assignment.routes.length === 0) {
|
|
934
|
+
next.roles[role] = {
|
|
935
|
+
inherit: true,
|
|
936
|
+
routes: []
|
|
937
|
+
};
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const kept = assignment.routes.filter((route) => selectable.has(routeKey(route.provider, route.model))).map((route) => stripIllegalEffort(route, live));
|
|
941
|
+
if (kept.length === 0) {
|
|
942
|
+
droppedRoles.push(role);
|
|
943
|
+
next.roles[role] = {
|
|
944
|
+
inherit: true,
|
|
945
|
+
routes: []
|
|
946
|
+
};
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
949
|
+
if (kept.length !== assignment.routes.length) droppedRoles.push(role);
|
|
950
|
+
next.roles[role] = {
|
|
951
|
+
inherit: false,
|
|
952
|
+
routes: isPanelRole(role) ? kept : [kept[0]]
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
overlay: next,
|
|
957
|
+
droppedRoles
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
//#endregion
|
|
961
|
+
//#region src/settings-api.ts
|
|
962
|
+
async function loadSettingsSnapshot(host) {
|
|
963
|
+
const dshHome = host.dshHome ?? resolveDshHome(host.env);
|
|
964
|
+
const catalog = await buildCatalog({
|
|
965
|
+
...host,
|
|
966
|
+
dshHome
|
|
967
|
+
});
|
|
968
|
+
const current = await readOverlay(dshHome);
|
|
969
|
+
const { overlay, droppedRoles } = dropUnselectableRoles(current.overlay, catalog.routes);
|
|
970
|
+
return {
|
|
971
|
+
catalog,
|
|
972
|
+
overlay,
|
|
973
|
+
path: current.path,
|
|
974
|
+
missing: current.missing,
|
|
975
|
+
droppedRoles
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
async function saveSettingsOverlay(host, raw) {
|
|
979
|
+
const dshHome = host.dshHome ?? resolveDshHome(host.env);
|
|
980
|
+
const overlay = typeof raw === "string" ? parseOverlay(raw) : parseOverlay(JSON.stringify(raw));
|
|
981
|
+
const errors = validateOverlayAgainstCatalog(overlay, (await buildCatalog({
|
|
982
|
+
...host,
|
|
983
|
+
dshHome
|
|
984
|
+
})).routes);
|
|
985
|
+
if (errors.length > 0) {
|
|
986
|
+
const error = /* @__PURE__ */ new Error(`overlay rejected against live catalog:\n${errors.join("\n")}`);
|
|
987
|
+
error.errors = errors;
|
|
988
|
+
throw error;
|
|
989
|
+
}
|
|
990
|
+
return {
|
|
991
|
+
path: await writeOverlay(overlay, dshHome),
|
|
992
|
+
overlay
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
//#endregion
|
|
996
|
+
//#region src/settings-routes.ts
|
|
997
|
+
function isRecord(value) {
|
|
998
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
999
|
+
}
|
|
1000
|
+
async function handleSnapshot(host, req, res) {
|
|
1001
|
+
if (req.method !== "GET") {
|
|
1002
|
+
json(res, 405, { error: "method not allowed" });
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
json(res, 200, await loadSettingsSnapshot(host));
|
|
1006
|
+
}
|
|
1007
|
+
async function handleSave(host, req, res) {
|
|
1008
|
+
if (req.method !== "PUT" && req.method !== "POST") {
|
|
1009
|
+
json(res, 405, { error: "method not allowed" });
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
const body = await readJson(req);
|
|
1014
|
+
json(res, 200, await saveSettingsOverlay(host, isRecord(body) && "overlay" in body ? body.overlay : body));
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
const message = error instanceof Error ? error.message : "save failed";
|
|
1017
|
+
const errors = error instanceof Error && "errors" in error && Array.isArray(error.errors) ? error.errors : void 0;
|
|
1018
|
+
json(res, 400, {
|
|
1019
|
+
error: message,
|
|
1020
|
+
...errors === void 0 ? {} : { errors }
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function registerPstackSettingsRoutes(ctx, host) {
|
|
1025
|
+
const webServer = ctx.webServer;
|
|
1026
|
+
if (webServer === void 0) return;
|
|
1027
|
+
ctx.effect(() => {
|
|
1028
|
+
const routes = [webServer.register({
|
|
1029
|
+
kind: "exact",
|
|
1030
|
+
path: SETTINGS_SNAPSHOT_PATH,
|
|
1031
|
+
handler: async (req, res) => {
|
|
1032
|
+
const rejection = ctx.connection.requestRejection(req);
|
|
1033
|
+
if (rejection !== void 0) {
|
|
1034
|
+
json(res, rejection, { error: rejection === 401 ? "unauthorized" : "forbidden" });
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (!trustedRequest(req)) {
|
|
1038
|
+
json(res, 403, { error: "forbidden" });
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
if (req.method === "GET") {
|
|
1042
|
+
await handleSnapshot(host, req, res);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
await handleSave(host, req, res);
|
|
1046
|
+
}
|
|
1047
|
+
})];
|
|
1048
|
+
return () => {
|
|
1049
|
+
for (const dispose of routes) dispose();
|
|
1050
|
+
};
|
|
1051
|
+
}, "pstack-dsh: Settings overlay routes");
|
|
1052
|
+
}
|
|
1053
|
+
//#endregion
|
|
1054
|
+
//#region src/index.ts
|
|
1055
|
+
const name = PLUGIN_ID;
|
|
1056
|
+
const inject = ["tools", "skills"];
|
|
1057
|
+
const JSON_OUTPUT = {
|
|
1058
|
+
schema: { type: "json" },
|
|
1059
|
+
render: (_args, value) => [{
|
|
1060
|
+
type: "text",
|
|
1061
|
+
text: JSON.stringify(value, null, 2)
|
|
1062
|
+
}]
|
|
1063
|
+
};
|
|
1064
|
+
function asHost(ctx, config, roles) {
|
|
1065
|
+
return {
|
|
1066
|
+
get llm() {
|
|
1067
|
+
return ctx.get("llm");
|
|
1068
|
+
},
|
|
1069
|
+
get credentials() {
|
|
1070
|
+
return ctx.get("credentials");
|
|
1071
|
+
},
|
|
1072
|
+
get settings() {
|
|
1073
|
+
return ctx.get("settings");
|
|
1074
|
+
},
|
|
1075
|
+
get subagents() {
|
|
1076
|
+
return ctx.get("subagents");
|
|
1077
|
+
},
|
|
1078
|
+
spawnProvider: config?.spawnProvider ?? "spawn",
|
|
1079
|
+
roles
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function registerOne(ctx, tool) {
|
|
1083
|
+
ctx.tools.register(defineTool({
|
|
1084
|
+
name: tool.name,
|
|
1085
|
+
description: tool.description,
|
|
1086
|
+
parameters: tool.parameters,
|
|
1087
|
+
output: JSON_OUTPUT,
|
|
1088
|
+
async execute(args, exec) {
|
|
1089
|
+
const snapshot = snapshotJsonValue(await tool.execute(args, exec));
|
|
1090
|
+
if (snapshot === void 0) throw new TypeError(`pstack tool ${tool.name} returned a non-JSON value`);
|
|
1091
|
+
return snapshot;
|
|
1092
|
+
}
|
|
1093
|
+
}));
|
|
1094
|
+
}
|
|
1095
|
+
function apply(ctx, config) {
|
|
1096
|
+
console.log("[my-plugins/pstack-dsh] loaded");
|
|
1097
|
+
const roles = new RoleEffortMap();
|
|
1098
|
+
const host = asHost(ctx, config, roles);
|
|
1099
|
+
registerOne(ctx, catalogTool(host));
|
|
1100
|
+
registerOne(ctx, overlayReadTool(host));
|
|
1101
|
+
registerOne(ctx, overlayWriteTool(host));
|
|
1102
|
+
registerOne(ctx, spawnTool(host));
|
|
1103
|
+
ctx.skills.registerProvider(() => createSkillProvider());
|
|
1104
|
+
ctx.inject(["webServer", "connection"], (webCtx) => {
|
|
1105
|
+
registerPstackSettingsRoutes(webCtx, host);
|
|
1106
|
+
});
|
|
1107
|
+
ctx.on("agent/request", async (payload, next) => {
|
|
1108
|
+
const base = await next();
|
|
1109
|
+
const binding = roles.lookup(payload.agent.id);
|
|
1110
|
+
if (binding === void 0) return base;
|
|
1111
|
+
return {
|
|
1112
|
+
...base,
|
|
1113
|
+
...binding.provider ? { provider: binding.provider } : {},
|
|
1114
|
+
...binding.model ? { model: binding.model } : {},
|
|
1115
|
+
...binding.reasoningEffort ? { reasoningEffort: ReasoningEffortId(binding.reasoningEffort) } : {}
|
|
1116
|
+
};
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
//#endregion
|
|
1120
|
+
export { Config, apply, inject, name };
|