@aexol/spectral 0.9.21 → 0.9.25
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/agents.d.ts +17 -1
- package/dist/agent/agents.d.ts.map +1 -1
- package/dist/agent/agents.js +42 -2
- package/dist/agent/index.d.ts +3 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +40 -12
- package/dist/cli.js +9 -3
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +14 -5
- package/dist/relay/dispatcher.d.ts +3 -1
- package/dist/relay/dispatcher.d.ts.map +1 -1
- package/dist/relay/dispatcher.js +23 -0
- package/dist/sdk/coding-agent/config.d.ts.map +1 -1
- package/dist/sdk/coding-agent/config.js +10 -1
- package/dist/server/agent-bridge.d.ts +8 -0
- package/dist/server/agent-bridge.d.ts.map +1 -1
- package/dist/server/agent-bridge.js +29 -1
- package/dist/server/handlers/agent-settings.d.ts +49 -0
- package/dist/server/handlers/agent-settings.d.ts.map +1 -0
- package/dist/server/handlers/agent-settings.js +109 -0
- package/dist/server/handlers/settings-lock.d.ts +36 -0
- package/dist/server/handlers/settings-lock.d.ts.map +1 -0
- package/dist/server/handlers/settings-lock.js +89 -0
- package/dist/server/handlers/settings.d.ts +0 -11
- package/dist/server/handlers/settings.d.ts.map +1 -1
- package/dist/server/handlers/settings.js +5 -85
- package/package.json +1 -1
package/dist/agent/agents.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* ---
|
|
7
7
|
* name: my-agent
|
|
8
8
|
* description: What this agent does
|
|
9
|
+
* mode: subagent # or "primary" to inject into the main system prompt
|
|
9
10
|
* tools: read, grep, find, ls
|
|
10
11
|
* model: claude-haiku-4-5
|
|
11
12
|
* ---
|
|
@@ -16,9 +17,11 @@
|
|
|
16
17
|
* .spectral/agents/*.md — Project-level (opt-in via agentScope)
|
|
17
18
|
*/
|
|
18
19
|
export type AgentScope = "user" | "project" | "both";
|
|
20
|
+
export type AgentMode = "primary" | "subagent";
|
|
19
21
|
export interface AgentConfig {
|
|
20
22
|
name: string;
|
|
21
23
|
description: string;
|
|
24
|
+
mode: AgentMode;
|
|
22
25
|
tools?: string[];
|
|
23
26
|
model?: string;
|
|
24
27
|
systemPrompt: string;
|
|
@@ -29,7 +32,20 @@ export interface AgentDiscoveryResult {
|
|
|
29
32
|
agents: AgentConfig[];
|
|
30
33
|
projectAgentsDir: string | null;
|
|
31
34
|
}
|
|
32
|
-
export declare function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult;
|
|
35
|
+
export declare function discoverAgents(cwd: string, scope: AgentScope, agentDir?: string): AgentDiscoveryResult;
|
|
36
|
+
export declare function discoverPrimaryAgent(cwd: string, scope?: AgentScope, agentDir?: string): AgentConfig | null;
|
|
37
|
+
/**
|
|
38
|
+
* Prepend the primary agent's system prompt above the base system prompt.
|
|
39
|
+
*
|
|
40
|
+
* Used by the per-turn `before_agent_start` hook so the primary-agent
|
|
41
|
+
* toggle takes effect mid-session (on the next user message) rather than
|
|
42
|
+
* only at session start.
|
|
43
|
+
*
|
|
44
|
+
* - Returns the base unchanged when no primary agent is provided.
|
|
45
|
+
* - Idempotent: if the base already starts with the primary agent header,
|
|
46
|
+
* the base is returned unchanged (guards against double-injection).
|
|
47
|
+
*/
|
|
48
|
+
export declare function injectPrimaryAgentPrompt(base: string | undefined, primaryAgent: Pick<AgentConfig, "name" | "description" | "systemPrompt"> | null): string | undefined;
|
|
33
49
|
export declare function formatAgentList(agents: AgentConfig[], maxItems: number): {
|
|
34
50
|
text: string;
|
|
35
51
|
remaining: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/agent/agents.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../src/agent/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAOH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AACrD,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;AAE/C,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AA2ED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,UAAU,EACjB,QAAQ,CAAC,EAAE,MAAM,GAChB,oBAAoB,CAoBtB;AAED,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,EACX,KAAK,GAAE,UAAmB,EAC1B,QAAQ,CAAC,EAAE,MAAM,GAChB,WAAW,GAAG,IAAI,CAgBpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,YAAY,EAAE,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,GAAG,cAAc,CAAC,GAAG,IAAI,GAC9E,MAAM,GAAG,SAAS,CAOpB;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,EAAE,EACrB,QAAQ,EAAE,MAAM,GACf;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAQrC"}
|
package/dist/agent/agents.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* ---
|
|
7
7
|
* name: my-agent
|
|
8
8
|
* description: What this agent does
|
|
9
|
+
* mode: subagent # or "primary" to inject into the main system prompt
|
|
9
10
|
* tools: read, grep, find, ls
|
|
10
11
|
* model: claude-haiku-4-5
|
|
11
12
|
* ---
|
|
@@ -17,6 +18,7 @@
|
|
|
17
18
|
*/
|
|
18
19
|
import * as fs from "node:fs";
|
|
19
20
|
import * as path from "node:path";
|
|
21
|
+
import chalk from "chalk";
|
|
20
22
|
import { getAgentDir, parseFrontmatter } from "../sdk/coding-agent/index.js";
|
|
21
23
|
function loadAgentsFromDir(dir, source) {
|
|
22
24
|
const agents = [];
|
|
@@ -51,9 +53,11 @@ function loadAgentsFromDir(dir, source) {
|
|
|
51
53
|
?.split(",")
|
|
52
54
|
.map((t) => t.trim())
|
|
53
55
|
.filter(Boolean);
|
|
56
|
+
const mode = frontmatter.mode === "primary" ? "primary" : "subagent";
|
|
54
57
|
agents.push({
|
|
55
58
|
name: frontmatter.name,
|
|
56
59
|
description: frontmatter.description,
|
|
60
|
+
mode,
|
|
57
61
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
58
62
|
model: frontmatter.model,
|
|
59
63
|
systemPrompt: body,
|
|
@@ -83,8 +87,8 @@ function findNearestProjectAgentsDir(cwd) {
|
|
|
83
87
|
currentDir = parentDir;
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
|
-
export function discoverAgents(cwd, scope) {
|
|
87
|
-
const userDir = path.join(getAgentDir(), "agents");
|
|
90
|
+
export function discoverAgents(cwd, scope, agentDir) {
|
|
91
|
+
const userDir = path.join(agentDir ?? getAgentDir(), "agents");
|
|
88
92
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
89
93
|
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
90
94
|
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
@@ -105,6 +109,42 @@ export function discoverAgents(cwd, scope) {
|
|
|
105
109
|
}
|
|
106
110
|
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
107
111
|
}
|
|
112
|
+
export function discoverPrimaryAgent(cwd, scope = "both", agentDir) {
|
|
113
|
+
const { agents } = discoverAgents(cwd, scope, agentDir);
|
|
114
|
+
const primaries = agents.filter((a) => a.mode === "primary");
|
|
115
|
+
if (primaries.length === 0)
|
|
116
|
+
return null;
|
|
117
|
+
if (primaries.length > 1) {
|
|
118
|
+
// User-level primary wins over project-level primary; otherwise first wins.
|
|
119
|
+
const userPrimary = primaries.find((a) => a.source === "user");
|
|
120
|
+
const chosen = userPrimary ?? primaries[0];
|
|
121
|
+
console.warn(chalk.yellow(`Multiple primary agents found (${primaries.map((a) => a.name).join(", ")}). Using "${chosen.name}".`));
|
|
122
|
+
return chosen;
|
|
123
|
+
}
|
|
124
|
+
return primaries[0];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Prepend the primary agent's system prompt above the base system prompt.
|
|
128
|
+
*
|
|
129
|
+
* Used by the per-turn `before_agent_start` hook so the primary-agent
|
|
130
|
+
* toggle takes effect mid-session (on the next user message) rather than
|
|
131
|
+
* only at session start.
|
|
132
|
+
*
|
|
133
|
+
* - Returns the base unchanged when no primary agent is provided.
|
|
134
|
+
* - Idempotent: if the base already starts with the primary agent header,
|
|
135
|
+
* the base is returned unchanged (guards against double-injection).
|
|
136
|
+
*/
|
|
137
|
+
export function injectPrimaryAgentPrompt(base, primaryAgent) {
|
|
138
|
+
if (!primaryAgent)
|
|
139
|
+
return base;
|
|
140
|
+
const header = `# Primary Agent: ${primaryAgent.name}\n\n${primaryAgent.description}`;
|
|
141
|
+
const block = `${header}\n\n${primaryAgent.systemPrompt}`;
|
|
142
|
+
if (!base)
|
|
143
|
+
return block;
|
|
144
|
+
if (base.startsWith(header))
|
|
145
|
+
return base;
|
|
146
|
+
return `${block}\n\n---\n\n${base}`;
|
|
147
|
+
}
|
|
108
148
|
export function formatAgentList(agents, maxItems) {
|
|
109
149
|
if (agents.length === 0)
|
|
110
150
|
return { text: "none", remaining: 0 };
|
package/dist/agent/index.d.ts
CHANGED
|
@@ -25,5 +25,7 @@
|
|
|
25
25
|
* - Access to observational memory via the recall tool
|
|
26
26
|
*/
|
|
27
27
|
import { type ExtensionAPI } from "../sdk/coding-agent/index.js";
|
|
28
|
-
export default function subagentExtension(ext: ExtensionAPI
|
|
28
|
+
export default function subagentExtension(ext: ExtensionAPI, opts?: {
|
|
29
|
+
disabledAgents?: string[];
|
|
30
|
+
}): void;
|
|
29
31
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAWH,OAAO,EACN,KAAK,YAAY,EAEjB,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/agent/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAWH,OAAO,EACN,KAAK,YAAY,EAEjB,MAAM,8BAA8B,CAAC;AAuetC,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,GAAG,EAAE,YAAY,EAAE,IAAI,CAAC,EAAE;IAAE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,IAAI,CA+UvG"}
|
package/dist/agent/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import { agentLoop } from "../sdk/agent-core/index.js";
|
|
|
28
28
|
import { StringEnum } from "../sdk/ai/index.js";
|
|
29
29
|
import { Type } from "typebox";
|
|
30
30
|
import { discoverAgents } from "./agents.js";
|
|
31
|
+
import { loadAgentSettings } from "../server/handlers/agent-settings.js";
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
// Built-in tool resolution (no spawn needed — tools are registered in-process)
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
@@ -175,7 +176,8 @@ async function mapWithConcurrencyLimit(items, concurrency, fn) {
|
|
|
175
176
|
// Core — run a single subagent in-process via agentLoop
|
|
176
177
|
// ---------------------------------------------------------------------------
|
|
177
178
|
async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, signal, onUpdate, makeDetails, ext, ctx) {
|
|
178
|
-
const
|
|
179
|
+
const normalizedAgentName = agentName.replace(/^@/, "");
|
|
180
|
+
const agent = agents.find((a) => a.name === normalizedAgentName);
|
|
179
181
|
if (!agent) {
|
|
180
182
|
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
181
183
|
return {
|
|
@@ -355,8 +357,8 @@ const ChainItem = Type.Object({
|
|
|
355
357
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
356
358
|
});
|
|
357
359
|
const AgentScopeSchema = StringEnum(["user", "project", "both"], {
|
|
358
|
-
description: 'Which agent directories to use. Default: "
|
|
359
|
-
default: "
|
|
360
|
+
description: 'Which agent directories to use. Default: "both" (user + project agents).',
|
|
361
|
+
default: "both",
|
|
360
362
|
});
|
|
361
363
|
const SubagentParams = Type.Object({
|
|
362
364
|
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (for single mode)" })),
|
|
@@ -379,15 +381,15 @@ const SubagentParams = Type.Object({
|
|
|
379
381
|
// ---------------------------------------------------------------------------
|
|
380
382
|
// Extension entry point
|
|
381
383
|
// ---------------------------------------------------------------------------
|
|
382
|
-
export default function subagentExtension(ext) {
|
|
384
|
+
export default function subagentExtension(ext, opts) {
|
|
383
385
|
ext.registerTool({
|
|
384
386
|
name: "subagent",
|
|
385
387
|
label: "Subagent",
|
|
386
388
|
description: [
|
|
387
389
|
"Delegate tasks to specialized subagents with isolated context.",
|
|
388
390
|
"Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
|
|
389
|
-
'Default agent scope is "
|
|
390
|
-
'
|
|
391
|
+
'Default agent scope is "both" (user agents from ~/.spectral/agent/agents + project agents from .spectral/agents).',
|
|
392
|
+
'Set agentScope: "user" or "project" to limit scope.'
|
|
391
393
|
].join(" "),
|
|
392
394
|
promptSnippet: "Run a subagent to investigate, plan, review, or implement code changes",
|
|
393
395
|
promptGuidelines: [
|
|
@@ -395,10 +397,36 @@ export default function subagentExtension(ext) {
|
|
|
395
397
|
],
|
|
396
398
|
parameters: SubagentParams,
|
|
397
399
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
398
|
-
|
|
400
|
+
// Re-load agent settings from disk on every call so that UI toggles
|
|
401
|
+
// (subagentsEnabled / disabledSubagents) take effect immediately on
|
|
402
|
+
// running sessions, not just on the next session start.
|
|
403
|
+
const liveSettings = loadAgentSettings(ctx.cwd);
|
|
404
|
+
if (!liveSettings.subagentsEnabled) {
|
|
405
|
+
return {
|
|
406
|
+
content: [
|
|
407
|
+
{
|
|
408
|
+
type: "text",
|
|
409
|
+
text: "Subagents are disabled in agent settings. Enable subagents via the agent settings panel to delegate tasks.",
|
|
410
|
+
},
|
|
411
|
+
],
|
|
412
|
+
details: {
|
|
413
|
+
mode: "single",
|
|
414
|
+
agentScope: (params.agentScope ?? "both"),
|
|
415
|
+
projectAgentsDir: null,
|
|
416
|
+
results: [],
|
|
417
|
+
},
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const agentScope = params.agentScope ?? "both";
|
|
399
421
|
const discovery = discoverAgents(ctx.cwd, agentScope);
|
|
400
|
-
|
|
401
|
-
|
|
422
|
+
// Merge registration-time disabled list (from session start) with the
|
|
423
|
+
// live disabled list (just read from disk) so both are honored.
|
|
424
|
+
const disabledSet = new Set([
|
|
425
|
+
...(opts?.disabledAgents ?? []),
|
|
426
|
+
...(liveSettings.disabledSubagents ?? []),
|
|
427
|
+
]);
|
|
428
|
+
const agents = discovery.agents.filter((a) => !disabledSet.has(a.name));
|
|
429
|
+
const confirmProjectAgents = params.confirmProjectAgents ?? false;
|
|
402
430
|
const hasChain = (params.chain?.length ?? 0) > 0;
|
|
403
431
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
404
432
|
const hasSingle = Boolean(params.agent && params.task);
|
|
@@ -430,12 +458,12 @@ export default function subagentExtension(ext) {
|
|
|
430
458
|
const requestedAgentNames = new Set();
|
|
431
459
|
if (params.chain)
|
|
432
460
|
for (const step of params.chain)
|
|
433
|
-
requestedAgentNames.add(step.agent);
|
|
461
|
+
requestedAgentNames.add(step.agent.replace(/^@/, ""));
|
|
434
462
|
if (params.tasks)
|
|
435
463
|
for (const t of params.tasks)
|
|
436
|
-
requestedAgentNames.add(t.agent);
|
|
464
|
+
requestedAgentNames.add(t.agent.replace(/^@/, ""));
|
|
437
465
|
if (params.agent)
|
|
438
|
-
requestedAgentNames.add(params.agent);
|
|
466
|
+
requestedAgentNames.add(params.agent.replace(/^@/, ""));
|
|
439
467
|
const projectAgentsRequested = Array.from(requestedAgentNames)
|
|
440
468
|
.map((name) => agents.find((a) => a.name === name))
|
|
441
469
|
.filter((a) => a?.source === "project");
|
package/dist/cli.js
CHANGED
|
@@ -18,9 +18,15 @@ import { dirname, resolve } from "node:path";
|
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
// ---- Read our own version ----------------------------------------------------
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
21
|
+
let VERSION = process.env.npm_package_version ?? "0.0.0";
|
|
22
|
+
try {
|
|
23
|
+
const pkgPath = resolve(__dirname, "..", "package.json");
|
|
24
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
25
|
+
VERSION = pkg.version;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Standalone Bun binaries do not carry the source package.json.
|
|
29
|
+
}
|
|
24
30
|
const TAGLINE = "Coding agent that never sleeps";
|
|
25
31
|
// ---- Branded helpers ---------------------------------------------------------
|
|
26
32
|
function printVersion() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAsBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AASlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/commands/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAsBH,OAAO,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AASlE,OAAO,KAAK,EAAE,aAAa,EAAc,MAAM,6BAA6B,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AAGnE,OAAO,EAAmB,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAsBrE;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAazD;AAED,MAAM,WAAW,eAAe;IAC9B,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAC;CACzB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,eAAe,CA6B9D;AAED,MAAM,WAAW,eAAe;IAC9B,iEAAiE;IACjE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4CAA4C;IAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,uCAAuC;IACvC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+DAA+D;IAC/D,aAAa,CAAC,EAAE,qBAAqB,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC;CAC/E;AAED,MAAM,WAAW,cAAc;IAC7B,qCAAqC;IACrC,KAAK,EAAE,YAAY,CAAC;IACpB,wDAAwD;IACxD,OAAO,EAAE,oBAAoB,CAAC;IAC9B,4CAA4C;IAC5C,KAAK,EAAE,WAAW,CAAC;IACnB,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAsB,QAAQ,CAAC,IAAI,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAsWlF;AAiED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAa5E"}
|
package/dist/commands/serve.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* in-process fake backend.
|
|
29
29
|
*/
|
|
30
30
|
import { readFileSync } from "node:fs";
|
|
31
|
-
import {
|
|
31
|
+
import { hostname } from "node:os";
|
|
32
32
|
import { dirname, join, resolve } from "node:path";
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
34
|
import { getConfigDir } from "../config.js";
|
|
@@ -49,9 +49,17 @@ const DEFAULT_BACKEND_URL = "https://api.aexol.ai";
|
|
|
49
49
|
function readPackageVersion() {
|
|
50
50
|
// dist/commands/serve.js → ../../package.json
|
|
51
51
|
// src/commands/serve.ts → ../../package.json (tsx / vitest)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
// In a Bun standalone binary the source tree is not present, so the
|
|
53
|
+
// package.json lookup below fails. Return a fallback version instead of
|
|
54
|
+
// crashing the serve process.
|
|
55
|
+
try {
|
|
56
|
+
const pkgPath = resolve(__dirname, "..", "..", "package.json");
|
|
57
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
58
|
+
return pkg.version;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return process.env.npm_package_version ?? "0.0.0";
|
|
62
|
+
}
|
|
55
63
|
}
|
|
56
64
|
/**
|
|
57
65
|
* Translate a backend HTTP(S) base URL to the relay WS(S) URL.
|
|
@@ -118,7 +126,7 @@ export function parseServeArgs(args) {
|
|
|
118
126
|
}
|
|
119
127
|
export async function runServe(opts = {}) {
|
|
120
128
|
const cfg = await requireLogin();
|
|
121
|
-
const cwd = opts.cwd ??
|
|
129
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
122
130
|
const version = readPackageVersion();
|
|
123
131
|
const installSignalHandlers = opts.installSignalHandlers ?? true;
|
|
124
132
|
const silent = opts.silent ?? false;
|
|
@@ -227,6 +235,7 @@ export async function runServe(opts = {}) {
|
|
|
227
235
|
store,
|
|
228
236
|
manager,
|
|
229
237
|
cwd,
|
|
238
|
+
agentDir: getAgentDir(),
|
|
230
239
|
publishMetaEvent: deferredPublishMetaEvent,
|
|
231
240
|
mcpState: getLiveMcpState() ?? undefined,
|
|
232
241
|
}).then((response) => {
|
|
@@ -48,7 +48,7 @@ import type { RelayClient } from "./client.js";
|
|
|
48
48
|
* `id` is populated when the route had an `:id` placeholder.
|
|
49
49
|
*/
|
|
50
50
|
interface RouteMatch {
|
|
51
|
-
route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "get_project_observations" | "list_project_sessions" | "create_session" | "get_session" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "pick_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "get_settings" | "put_settings";
|
|
51
|
+
route: "list_projects" | "create_project" | "update_project" | "delete_project" | "bind_studio" | "get_project_observations" | "list_project_sessions" | "create_session" | "get_session" | "update_session" | "delete_session" | "get_session_memory" | "get_session_memory_details" | "compact_session" | "remember_and_delete_session" | "fork_session" | "list_path_autocomplete" | "pick_directory" | "search_project_files" | "enqueue_prompt" | "get_prompt_queue" | "remove_prompt" | "clear_prompt_queue" | "list_mcp_status" | "get_settings" | "put_settings" | "get_agent_settings" | "put_agent_settings" | "list_agents";
|
|
52
52
|
id?: string;
|
|
53
53
|
/** Parsed query params, if the path carried a `?...` suffix. */
|
|
54
54
|
query?: URLSearchParams;
|
|
@@ -79,6 +79,8 @@ export interface RestRequestDeps {
|
|
|
79
79
|
manager: SessionStreamManager;
|
|
80
80
|
/** Working directory for this machine. Used by settings persistence. Defaults to homedir. */
|
|
81
81
|
cwd?: string;
|
|
82
|
+
/** Agent config dir (`~/.spectral/agent` by default). Passed through to agent-settings persistence. */
|
|
83
|
+
agentDir?: string;
|
|
82
84
|
/** Optional logger for unexpected errors. Defaults to console.error. */
|
|
83
85
|
logger?: {
|
|
84
86
|
error: (...args: unknown[]) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAIH,OAAO,KAAK,EACV,iBAAiB,EACjB,eAAe,EACf,kBAAkB,EAClB,SAAS,EACT,gBAAgB,EAChB,iBAAiB,EACjB,cAAc,EACf,MAAM,uBAAuB,CAAC;AAI/B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"dispatcher.d.ts","sourceRoot":"","sources":["../../src/relay/dispatcher.ts"],"names":[],"mappings":"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"}
|
package/dist/relay/dispatcher.js
CHANGED
|
@@ -47,6 +47,7 @@ import { handleSearchProjectFiles } from "../server/handlers/files-search.js";
|
|
|
47
47
|
import { handleBindStudioProject, handleCreateProject, handleDeleteProject, handleGetProjectObservations, handleListProjects, handleListSessionsByProject, handleUpdateProject, } from "../server/handlers/projects.js";
|
|
48
48
|
import { handleCompactSession, handleCreateSession, handleDeleteSession, handleForkSession, handleRememberAndDeleteSession, handleGetSessionDetail, handleGetSessionMemoryDetails, handleGetSessionMemoryStatus, handleUpdateSession, } from "../server/handlers/sessions.js";
|
|
49
49
|
import { handleClearPromptQueue, handleEnqueuePrompt, handleGetPromptQueue, handleRemovePrompt, } from "../server/handlers/queue.js";
|
|
50
|
+
import { handleGetAgentSettings, handleListAgents, handlePutAgentSettings, } from "../server/handlers/agent-settings.js";
|
|
50
51
|
import { handleGetSettings, handlePutSettings, } from "../server/handlers/settings.js";
|
|
51
52
|
import { shutdownState } from "../server/shutdown.js";
|
|
52
53
|
import { handleAutoResearch } from "./auto-research.js";
|
|
@@ -76,6 +77,20 @@ export function matchRoute(method, path) {
|
|
|
76
77
|
return { route: "put_settings" };
|
|
77
78
|
return null;
|
|
78
79
|
}
|
|
80
|
+
// /api/agent-settings
|
|
81
|
+
if (cleanPath === "/api/agent-settings") {
|
|
82
|
+
if (method === "GET")
|
|
83
|
+
return { route: "get_agent_settings" };
|
|
84
|
+
if (method === "PUT")
|
|
85
|
+
return { route: "put_agent_settings" };
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
// /api/agents
|
|
89
|
+
if (cleanPath === "/api/agents") {
|
|
90
|
+
if (method === "GET")
|
|
91
|
+
return { route: "list_agents", query };
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
79
94
|
// /api/paths/autocomplete
|
|
80
95
|
if (cleanPath === "/api/paths/autocomplete") {
|
|
81
96
|
if (method === "GET")
|
|
@@ -451,6 +466,14 @@ async function dispatchRoute(match, body, deps) {
|
|
|
451
466
|
return handleGetSettings(deps.cwd ?? homedir());
|
|
452
467
|
case "put_settings":
|
|
453
468
|
return handlePutSettings(deps.cwd ?? homedir(), asObject(body));
|
|
469
|
+
case "get_agent_settings":
|
|
470
|
+
return handleGetAgentSettings(deps.cwd ?? homedir(), deps.agentDir);
|
|
471
|
+
case "put_agent_settings":
|
|
472
|
+
return handlePutAgentSettings(deps.cwd ?? homedir(), asObject(body), deps.agentDir);
|
|
473
|
+
case "list_agents": {
|
|
474
|
+
const agentCwd = match.query?.get("cwd") || deps.cwd || homedir();
|
|
475
|
+
return handleListAgents(agentCwd, deps.agentDir);
|
|
476
|
+
}
|
|
454
477
|
}
|
|
455
478
|
}
|
|
456
479
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/sdk/coding-agent/config.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE,UAAU,qBAAqB;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAkB,SAAQ,qBAAqB;IAC/D,KAAK,CAAC,EAAE,qBAAqB,EAAE,CAAC;CAChC;AAsBD,wBAAgB,mBAAmB,IAAI,aAAa,CAcnD;AAyKD,wBAAgB,oBAAoB,CACnC,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,iBAAiB,SAAc,GAC7B,iBAAiB,GAAG,SAAS,CAO/B;AAED,wBAAgB,mCAAmC,CAClD,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,iBAAiB,SAAc,GAC7B,MAAM,CAUR;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAOhE;AAMD;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAiBtC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAKrC;AAED;;;;+BAI+B;AAC/B,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,4BAA4B;AAC5B,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,iCAAiC;AACjC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,qCAAqC;AACrC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,+BAA+B;AAC/B,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/sdk/coding-agent/config.ts"],"names":[],"mappings":"AAoBA,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEhE,UAAU,qBAAqB;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAkB,SAAQ,qBAAqB;IAC/D,KAAK,CAAC,EAAE,qBAAqB,EAAE,CAAC;CAChC;AAsBD,wBAAgB,mBAAmB,IAAI,aAAa,CAcnD;AAyKD,wBAAgB,oBAAoB,CACnC,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,iBAAiB,SAAc,GAC7B,iBAAiB,GAAG,SAAS,CAO/B;AAED,wBAAgB,mCAAmC,CAClD,WAAW,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,EAAE,EACrB,iBAAiB,SAAc,GAC7B,MAAM,CAUR;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAOhE;AAMD;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAiBtC;AAED;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAKrC;AAED;;;;+BAI+B;AAC/B,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,4BAA4B;AAC5B,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,iCAAiC;AACjC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,qCAAqC;AACrC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,+BAA+B;AAC/B,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AA4BD,eAAO,MAAM,YAAY,EAAE,MAA+B,CAAC;AAC3D,eAAO,MAAM,QAAQ,EAAE,MAAyC,CAAC;AACjE,eAAO,MAAM,SAAS,EAAE,MAAmD,CAAC;AAC5E,eAAO,MAAM,eAAe,EAAE,MAAqD,CAAC;AACpF,eAAO,MAAM,OAAO,EAAE,MAA+B,CAAC;AAGtD,eAAO,MAAM,aAAa,QAA+C,CAAC;AAC1E,eAAO,MAAM,eAAe,QAAuD,CAAC;AAEpF,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEpD;AAID,6CAA6C;AAC7C,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGxD;AAMD,gEAAgE;AAChE,wBAAgB,WAAW,IAAI,MAAM,CAMpC;AAED,iDAAiD;AACjD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,8BAA8B;AAC9B,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,4BAA4B;AAC5B,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,gCAAgC;AAChC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED,kCAAkC;AAClC,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAED,sDAAsD;AACtD,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,6CAA6C;AAC7C,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,qCAAqC;AACrC,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,iCAAiC;AACjC,wBAAgB,eAAe,IAAI,MAAM,CAExC"}
|
|
@@ -268,7 +268,16 @@ export function getExamplesPath() {
|
|
|
268
268
|
export function getChangelogPath() {
|
|
269
269
|
return resolve(join(getPackageDir(), "CHANGELOG.md"));
|
|
270
270
|
}
|
|
271
|
-
|
|
271
|
+
function readPackageJson() {
|
|
272
|
+
try {
|
|
273
|
+
return JSON.parse(readFileSync(getPackageJsonPath(), "utf-8"));
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// Standalone Bun binaries do not carry the source package.json.
|
|
277
|
+
return {};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const pkg = readPackageJson();
|
|
272
281
|
const spectralConfigName = pkg.spectralConfig?.name;
|
|
273
282
|
export const PACKAGE_NAME = pkg.name || "index.ts";
|
|
274
283
|
export const APP_NAME = spectralConfigName || "spectral";
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
* instance is reused across `prompt()` calls).
|
|
50
50
|
*/
|
|
51
51
|
import { type AgentSessionEvent } from "../sdk/coding-agent/index.js";
|
|
52
|
+
import { type AgentSettings } from "./handlers/agent-settings.js";
|
|
52
53
|
import { fetchAllowedModels as defaultFetchAllowedModels } from "../relay/models-fetch.js";
|
|
53
54
|
import type { SessionMemorySnapshot } from "./storage.js";
|
|
54
55
|
import type { ImageAttachment, ServerEvent, WireMessage } from "./wire.js";
|
|
@@ -116,6 +117,13 @@ export interface AgentBridgeOptions {
|
|
|
116
117
|
* can run without a live backend.
|
|
117
118
|
*/
|
|
118
119
|
fetchAllowedModels?: FetchAllowedModelsFn;
|
|
120
|
+
/**
|
|
121
|
+
* Agent-behavior toggles (primary-agent injection, subagent tool).
|
|
122
|
+
* When omitted, settings are loaded from `settings.json` at `start()`
|
|
123
|
+
* time. Changes take effect on the next session/bridge — a running
|
|
124
|
+
* session is not hot-reloaded.
|
|
125
|
+
*/
|
|
126
|
+
agentSettings?: AgentSettings;
|
|
119
127
|
}
|
|
120
128
|
export declare class AgentBridge {
|
|
121
129
|
private session?;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAcH,OAAO,EAQL,KAAK,iBAAiB,EAEvB,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../../src/server/agent-bridge.ts"],"names":[],"mappings":"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"}
|
|
@@ -55,6 +55,8 @@ import { existsSync, statSync } from "node:fs";
|
|
|
55
55
|
import { dirname, join, resolve } from "node:path";
|
|
56
56
|
import { fileURLToPath } from "node:url";
|
|
57
57
|
import aexolMcpExtension from "../extensions/aexol-mcp.js";
|
|
58
|
+
import { discoverPrimaryAgent, injectPrimaryAgentPrompt } from "../agent/agents.js";
|
|
59
|
+
import { loadAgentSettings } from "./handlers/agent-settings.js";
|
|
58
60
|
import kanbanBridgeExtension from "../extensions/kanban-bridge.js";
|
|
59
61
|
import spectralVisionExtension from "../extensions/spectral-vision-fallback.js";
|
|
60
62
|
import subagentExt from "../agent/index.js";
|
|
@@ -417,7 +419,30 @@ export class AgentBridge {
|
|
|
417
419
|
async start() {
|
|
418
420
|
if (this.disposed)
|
|
419
421
|
throw new Error("AgentBridge already disposed");
|
|
420
|
-
const extensionFactories = [
|
|
422
|
+
const extensionFactories = [
|
|
423
|
+
// Primary-agent prompt is injected per-turn via before_agent_start so the
|
|
424
|
+
// primaryAgentEnabled toggle takes effect mid-session (next user message).
|
|
425
|
+
async (ext) => {
|
|
426
|
+
ext.on("before_agent_start", (event) => {
|
|
427
|
+
const settings = loadAgentSettings(this.opts.cwd, this.opts.agentDir);
|
|
428
|
+
if (!settings.primaryAgentEnabled)
|
|
429
|
+
return;
|
|
430
|
+
const primaryAgent = discoverPrimaryAgent(this.opts.cwd, "both", this.opts.agentDir);
|
|
431
|
+
const injected = injectPrimaryAgentPrompt(event.systemPrompt, primaryAgent);
|
|
432
|
+
if (injected === event.systemPrompt)
|
|
433
|
+
return;
|
|
434
|
+
return { systemPrompt: injected };
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
aexolMcpExtension, kanbanBridgeExtension, async (ext) => { spectralVisionExtension(ext); }, async (ext) => { designerExtension(ext); }, async (ext) => { observationalMemory(ext); }
|
|
438
|
+
];
|
|
439
|
+
// Agent-behavior toggles. Loaded once at bridge construction so changes
|
|
440
|
+
// take effect on the next session (not mid-turn).
|
|
441
|
+
const agentSettings = this.opts.agentSettings ?? loadAgentSettings(this.opts.cwd, this.opts.agentDir);
|
|
442
|
+
// Only register the subagent tool when the toggle is enabled.
|
|
443
|
+
if (agentSettings.subagentsEnabled) {
|
|
444
|
+
extensionFactories.splice(4, 0, async (ext) => { subagentExt(ext, { disabledAgents: agentSettings.disabledSubagents }); });
|
|
445
|
+
}
|
|
421
446
|
// Load spectral-mcp-adapter via jiti so tsc never crawls its .ts files in
|
|
422
447
|
// node_modules. The static `import` was causing tsc to type-check
|
|
423
448
|
// spectral-mcp-adapter's source and fail the build on its type errors.
|
|
@@ -448,6 +473,9 @@ export class AgentBridge {
|
|
|
448
473
|
// .opencode/skills and .aexol/skills so OpenCode/Codex/Aexol skills
|
|
449
474
|
// work out of the box.
|
|
450
475
|
const extraSkillPaths = collectAncestorSkillDirs(this.opts.cwd, [".opencode/skills", ".aexol/skills"]);
|
|
476
|
+
// Primary-agent prompt injection happens per-turn via the before_agent_start
|
|
477
|
+
// extension registered above, so the primaryAgentEnabled toggle takes effect
|
|
478
|
+
// mid-session (on the next user message) rather than only at session start.
|
|
451
479
|
const resourceLoader = new DefaultResourceLoader({
|
|
452
480
|
cwd: this.opts.cwd,
|
|
453
481
|
agentDir: this.opts.agentDir ?? `${process.env.HOME ?? ""}/.spectral/agent`,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent-behavior toggles stored under the `agent` namespace in
|
|
3
|
+
* `settings.json`. These control whether the primary-agent system prompt
|
|
4
|
+
* is injected and whether the `subagent` tool is registered.
|
|
5
|
+
*
|
|
6
|
+
* Both default to `true` so existing behaviour is preserved when the
|
|
7
|
+
* namespace is absent.
|
|
8
|
+
*/
|
|
9
|
+
export interface AgentSettings {
|
|
10
|
+
primaryAgentEnabled: boolean;
|
|
11
|
+
subagentsEnabled: boolean;
|
|
12
|
+
/** Names of individual subagents that have been explicitly disabled. */
|
|
13
|
+
disabledSubagents: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare const AGENT_SETTINGS_DEFAULTS: AgentSettings;
|
|
16
|
+
export interface PutAgentSettingsInput {
|
|
17
|
+
primaryAgentEnabled?: unknown;
|
|
18
|
+
subagentsEnabled?: unknown;
|
|
19
|
+
disabledSubagents?: unknown;
|
|
20
|
+
}
|
|
21
|
+
export interface PutAgentSettingsOutput extends AgentSettings {
|
|
22
|
+
ok: true;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Load agent settings by merging defaults ← global ← project overrides.
|
|
26
|
+
* Mirrors the precedence pattern used by `loadConfig` for observational
|
|
27
|
+
* memory config.
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadAgentSettings(cwd: string, agentDir?: string): AgentSettings;
|
|
30
|
+
export declare function handleGetAgentSettings(cwd: string, agentDir?: string): AgentSettings;
|
|
31
|
+
/**
|
|
32
|
+
* Persist a partial update to the `agent` namespace in the **global**
|
|
33
|
+
* `settings.json` (per-machine, matching the existing memory-settings
|
|
34
|
+
* behaviour). Project-level overrides are read-only from the REST API.
|
|
35
|
+
*
|
|
36
|
+
* Only boolean values are accepted for the two known keys; anything else
|
|
37
|
+
* is a `400 BadRequestError`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function handlePutAgentSettings(cwd: string, body: PutAgentSettingsInput, agentDir?: string): PutAgentSettingsOutput;
|
|
40
|
+
export interface AgentInfo {
|
|
41
|
+
name: string;
|
|
42
|
+
description: string;
|
|
43
|
+
mode: string;
|
|
44
|
+
source: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function handleListAgents(cwd: string, agentDir?: string): {
|
|
47
|
+
agents: AgentInfo[];
|
|
48
|
+
};
|
|
49
|
+
//# sourceMappingURL=agent-settings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-settings.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/agent-settings.ts"],"names":[],"mappings":"AAOA;;;;;;;GAOG;AACH,MAAM,WAAW,aAAa;IAC5B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,wEAAwE;IACxE,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAED,eAAO,MAAM,uBAAuB,EAAE,aAIrC,CAAC;AAKF,MAAM,WAAW,qBAAqB;IACpC,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAuB,SAAQ,aAAa;IAC3D,EAAE,EAAE,IAAI,CAAC;CACV;AA+BD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,GAChB,aAAa,CAQf;AAED,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,GAChB,aAAa,CAEf;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,qBAAqB,EAC3B,QAAQ,CAAC,EAAE,MAAM,GAChB,sBAAsB,CAuCxB;AAMD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,QAAQ,CAAC,EAAE,MAAM,GAChB;IAAE,MAAM,EAAE,SAAS,EAAE,CAAA;CAAE,CAUzB"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "../../sdk/coding-agent/index.js";
|
|
4
|
+
import { discoverAgents } from "../../agent/agents.js";
|
|
5
|
+
import { BadRequestError } from "./errors.js";
|
|
6
|
+
import { withSettingsLock } from "./settings-lock.js";
|
|
7
|
+
export const AGENT_SETTINGS_DEFAULTS = {
|
|
8
|
+
primaryAgentEnabled: true,
|
|
9
|
+
subagentsEnabled: true,
|
|
10
|
+
disabledSubagents: [],
|
|
11
|
+
};
|
|
12
|
+
const AGENT_NAMESPACE = "agent";
|
|
13
|
+
const ALLOWED_KEYS = new Set(["primaryAgentEnabled", "subagentsEnabled", "disabledSubagents"]);
|
|
14
|
+
/**
|
|
15
|
+
* Read the `agent` namespace from a single `settings.json` file.
|
|
16
|
+
* Returns `{}` when the file or namespace is absent or unparseable.
|
|
17
|
+
*/
|
|
18
|
+
function readAgentNamespace(path) {
|
|
19
|
+
if (!existsSync(path))
|
|
20
|
+
return {};
|
|
21
|
+
try {
|
|
22
|
+
const raw = JSON.parse(readFileSync(path, "utf-8"));
|
|
23
|
+
const ns = raw[AGENT_NAMESPACE];
|
|
24
|
+
if (!ns || typeof ns !== "object")
|
|
25
|
+
return {};
|
|
26
|
+
const obj = ns;
|
|
27
|
+
const result = {};
|
|
28
|
+
if (typeof obj.primaryAgentEnabled === "boolean") {
|
|
29
|
+
result.primaryAgentEnabled = obj.primaryAgentEnabled;
|
|
30
|
+
}
|
|
31
|
+
if (typeof obj.subagentsEnabled === "boolean") {
|
|
32
|
+
result.subagentsEnabled = obj.subagentsEnabled;
|
|
33
|
+
}
|
|
34
|
+
if (Array.isArray(obj.disabledSubagents)) {
|
|
35
|
+
result.disabledSubagents = obj.disabledSubagents.filter((s) => typeof s === "string");
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Load agent settings by merging defaults ← global ← project overrides.
|
|
45
|
+
* Mirrors the precedence pattern used by `loadConfig` for observational
|
|
46
|
+
* memory config.
|
|
47
|
+
*/
|
|
48
|
+
export function loadAgentSettings(cwd, agentDir) {
|
|
49
|
+
const globalPath = join(agentDir ?? getAgentDir(), "settings.json");
|
|
50
|
+
const projectPath = join(cwd, ".spectral", "settings.json");
|
|
51
|
+
return {
|
|
52
|
+
...AGENT_SETTINGS_DEFAULTS,
|
|
53
|
+
...readAgentNamespace(globalPath),
|
|
54
|
+
...readAgentNamespace(projectPath),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function handleGetAgentSettings(cwd, agentDir) {
|
|
58
|
+
return loadAgentSettings(cwd, agentDir);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Persist a partial update to the `agent` namespace in the **global**
|
|
62
|
+
* `settings.json` (per-machine, matching the existing memory-settings
|
|
63
|
+
* behaviour). Project-level overrides are read-only from the REST API.
|
|
64
|
+
*
|
|
65
|
+
* Only boolean values are accepted for the two known keys; anything else
|
|
66
|
+
* is a `400 BadRequestError`.
|
|
67
|
+
*/
|
|
68
|
+
export function handlePutAgentSettings(cwd, body, agentDir) {
|
|
69
|
+
const patch = {};
|
|
70
|
+
for (const key of ["primaryAgentEnabled", "subagentsEnabled"]) {
|
|
71
|
+
if (body[key] !== undefined) {
|
|
72
|
+
if (typeof body[key] !== "boolean") {
|
|
73
|
+
throw new BadRequestError(`agent setting "${key}" must be a boolean, got ${typeof body[key]}`);
|
|
74
|
+
}
|
|
75
|
+
patch[key] = body[key];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// disabledSubagents is a full-replacement string array
|
|
79
|
+
if (body.disabledSubagents !== undefined) {
|
|
80
|
+
if (!Array.isArray(body.disabledSubagents) ||
|
|
81
|
+
!body.disabledSubagents.every((s) => typeof s === "string")) {
|
|
82
|
+
throw new BadRequestError(`agent setting "disabledSubagents" must be a string array`);
|
|
83
|
+
}
|
|
84
|
+
patch.disabledSubagents = body.disabledSubagents;
|
|
85
|
+
}
|
|
86
|
+
if (Object.keys(patch).length === 0) {
|
|
87
|
+
throw new BadRequestError(`agent settings body must include at least one of: ${[...ALLOWED_KEYS].join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
// Persist to the global file only. The lock is shared with the
|
|
90
|
+
// observational-memory handler so concurrent writes cannot corrupt
|
|
91
|
+
// each other's namespace.
|
|
92
|
+
withSettingsLock({ agentDir, namespace: AGENT_NAMESPACE }, (current) => {
|
|
93
|
+
return { ...current, ...patch };
|
|
94
|
+
});
|
|
95
|
+
// Return the fully resolved settings (global + project merge) so the
|
|
96
|
+
// caller sees the effective state, not just what we wrote.
|
|
97
|
+
return { ok: true, ...loadAgentSettings(cwd, agentDir) };
|
|
98
|
+
}
|
|
99
|
+
export function handleListAgents(cwd, agentDir) {
|
|
100
|
+
const { agents } = discoverAgents(cwd, "both", agentDir);
|
|
101
|
+
return {
|
|
102
|
+
agents: agents.map((a) => ({
|
|
103
|
+
name: a.name,
|
|
104
|
+
description: a.description,
|
|
105
|
+
mode: a.mode,
|
|
106
|
+
source: a.source,
|
|
107
|
+
})),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BadRequestError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Acquire a synchronous file lock with bounded retries on `ELOCKED`.
|
|
4
|
+
*
|
|
5
|
+
* Shared by all settings handlers that persist to `settings.json` so
|
|
6
|
+
* concurrent writes from different namespaces (observational-memory,
|
|
7
|
+
* agent) cannot interleave and corrupt the file.
|
|
8
|
+
*/
|
|
9
|
+
export declare function acquireLockSyncWithRetry(path: string): () => void;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the `settings.json` path for a given agent directory.
|
|
12
|
+
* Falls back to the default agent dir (`~/.spectral/agent`) when
|
|
13
|
+
* `agentDir` is omitted.
|
|
14
|
+
*/
|
|
15
|
+
export declare function settingsFilePath(agentDir?: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Read-persist cycle for a single namespace inside `settings.json`,
|
|
18
|
+
* guarded by a file-level lock so concurrent writes across namespaces
|
|
19
|
+
* are serialized.
|
|
20
|
+
*
|
|
21
|
+
* - Reads the **entire** file (preserving all top-level keys).
|
|
22
|
+
* - Extracts `parsed[namespace]` (or `{}` if absent).
|
|
23
|
+
* - Passes the current namespace object to `fn`. If `fn` returns
|
|
24
|
+
* `undefined`, no write occurs. Otherwise the returned object
|
|
25
|
+
* replaces the namespace and the full file is written back.
|
|
26
|
+
*
|
|
27
|
+
* The lock is acquired **before** writing (and also before reading if
|
|
28
|
+
* the file already exists) so we never observe a half-written file.
|
|
29
|
+
*/
|
|
30
|
+
export declare function withSettingsLock(opts: {
|
|
31
|
+
agentDir?: string;
|
|
32
|
+
namespace: string;
|
|
33
|
+
}, fn: (current: Record<string, unknown>) => Record<string, unknown> | undefined): void;
|
|
34
|
+
/** Re-exported for handler convenience. */
|
|
35
|
+
export { BadRequestError };
|
|
36
|
+
//# sourceMappingURL=settings-lock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings-lock.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/settings-lock.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,IAAI,CAsBjE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,EAC9C,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,GAC5E,IAAI,CA4BN;AAED,2CAA2C;AAC3C,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import lockfile from "proper-lockfile";
|
|
4
|
+
import { getAgentDir } from "../../sdk/coding-agent/index.js";
|
|
5
|
+
import { BadRequestError } from "./errors.js";
|
|
6
|
+
/**
|
|
7
|
+
* Acquire a synchronous file lock with bounded retries on `ELOCKED`.
|
|
8
|
+
*
|
|
9
|
+
* Shared by all settings handlers that persist to `settings.json` so
|
|
10
|
+
* concurrent writes from different namespaces (observational-memory,
|
|
11
|
+
* agent) cannot interleave and corrupt the file.
|
|
12
|
+
*/
|
|
13
|
+
export function acquireLockSyncWithRetry(path) {
|
|
14
|
+
const maxAttempts = 10;
|
|
15
|
+
const delayMs = 20;
|
|
16
|
+
let lastError;
|
|
17
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
18
|
+
try {
|
|
19
|
+
return lockfile.lockSync(path, { realpath: false });
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
23
|
+
? String(error.code)
|
|
24
|
+
: undefined;
|
|
25
|
+
if (code !== "ELOCKED" || attempt === maxAttempts) {
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
lastError = error;
|
|
29
|
+
const start = Date.now();
|
|
30
|
+
while (Date.now() - start < delayMs) {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
throw lastError ?? new Error("Failed to acquire settings lock");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the `settings.json` path for a given agent directory.
|
|
38
|
+
* Falls back to the default agent dir (`~/.spectral/agent`) when
|
|
39
|
+
* `agentDir` is omitted.
|
|
40
|
+
*/
|
|
41
|
+
export function settingsFilePath(agentDir) {
|
|
42
|
+
return join(agentDir ?? getAgentDir(), "settings.json");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read-persist cycle for a single namespace inside `settings.json`,
|
|
46
|
+
* guarded by a file-level lock so concurrent writes across namespaces
|
|
47
|
+
* are serialized.
|
|
48
|
+
*
|
|
49
|
+
* - Reads the **entire** file (preserving all top-level keys).
|
|
50
|
+
* - Extracts `parsed[namespace]` (or `{}` if absent).
|
|
51
|
+
* - Passes the current namespace object to `fn`. If `fn` returns
|
|
52
|
+
* `undefined`, no write occurs. Otherwise the returned object
|
|
53
|
+
* replaces the namespace and the full file is written back.
|
|
54
|
+
*
|
|
55
|
+
* The lock is acquired **before** writing (and also before reading if
|
|
56
|
+
* the file already exists) so we never observe a half-written file.
|
|
57
|
+
*/
|
|
58
|
+
export function withSettingsLock(opts, fn) {
|
|
59
|
+
const path = settingsFilePath(opts.agentDir);
|
|
60
|
+
const dir = dirname(path);
|
|
61
|
+
let release;
|
|
62
|
+
try {
|
|
63
|
+
const fileExists = existsSync(path);
|
|
64
|
+
if (fileExists) {
|
|
65
|
+
release = acquireLockSyncWithRetry(path);
|
|
66
|
+
}
|
|
67
|
+
const raw = fileExists ? readFileSync(path, "utf-8") : "{}";
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
const ns = (parsed[opts.namespace] ?? {});
|
|
70
|
+
const next = fn(ns);
|
|
71
|
+
if (next !== undefined) {
|
|
72
|
+
parsed[opts.namespace] = { ...ns, ...next };
|
|
73
|
+
if (!existsSync(dir)) {
|
|
74
|
+
mkdirSync(dir, { recursive: true });
|
|
75
|
+
}
|
|
76
|
+
if (!release) {
|
|
77
|
+
release = acquireLockSyncWithRetry(path);
|
|
78
|
+
}
|
|
79
|
+
writeFileSync(path, JSON.stringify(parsed, null, 2), "utf-8");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
if (release) {
|
|
84
|
+
release();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Re-exported for handler convenience. */
|
|
89
|
+
export { BadRequestError };
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure REST handler for `GET /api/settings` and `PUT /api/settings`.
|
|
3
|
-
*
|
|
4
|
-
* Settings are stored in the machine's global `settings.json` under the
|
|
5
|
-
* `"observational-memory"` namespace (see `src/memory/config.ts`).
|
|
6
|
-
*
|
|
7
|
-
* GET /api/settings → returns full observational-memory config
|
|
8
|
-
* PUT /api/settings → updates a single key via `{ key, value }` body
|
|
9
|
-
*
|
|
10
|
-
* All paths are machine-level — no session context needed.
|
|
11
|
-
*/
|
|
12
1
|
export interface PutSettingsInput {
|
|
13
2
|
key?: unknown;
|
|
14
3
|
value?: unknown;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/settings.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../../src/server/handlers/settings.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AACD,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,IAAI,CAAC;IACT,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AACD,MAAM,WAAW,iBAAiB;IAChC,0BAA0B,EAAE,MAAM,CAAC;IACnC,yBAAyB,EAAE,MAAM,CAAC;IAClC,yBAAyB,EAAE,MAAM,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAgBD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,iBAAiB,CAShE;AACD,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,gBAAgB,GACrB,iBAAiB,CAoBnB"}
|
|
@@ -1,93 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure REST handler for `GET /api/settings` and `PUT /api/settings`.
|
|
3
|
-
*
|
|
4
|
-
* Settings are stored in the machine's global `settings.json` under the
|
|
5
|
-
* `"observational-memory"` namespace (see `src/memory/config.ts`).
|
|
6
|
-
*
|
|
7
|
-
* GET /api/settings → returns full observational-memory config
|
|
8
|
-
* PUT /api/settings → updates a single key via `{ key, value }` body
|
|
9
|
-
*
|
|
10
|
-
* All paths are machine-level — no session context needed.
|
|
11
|
-
*/
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { dirname, join } from "node:path";
|
|
14
|
-
import lockfile from "proper-lockfile";
|
|
15
|
-
import { getAgentDir } from "../../sdk/coding-agent/index.js";
|
|
16
1
|
import { loadConfig } from "../../memory/config.js";
|
|
17
2
|
import { BadRequestError } from "./errors.js";
|
|
18
|
-
|
|
3
|
+
import { withSettingsLock } from "./settings-lock.js";
|
|
19
4
|
const ALLOWED_KEYS = new Set([
|
|
20
5
|
"observationThresholdTokens",
|
|
21
6
|
"compactionThresholdTokens",
|
|
22
7
|
"reflectionThresholdTokens",
|
|
23
8
|
]);
|
|
24
|
-
/** Sane bounds for token thresholds. */
|
|
25
9
|
const MIN_TOKENS = 1_000;
|
|
26
10
|
const MAX_TOKENS = 1_000_000;
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// File helpers (inlined to avoid pulling in the full SettingsManager)
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
function acquireLockSyncWithRetry(path) {
|
|
31
|
-
const maxAttempts = 10;
|
|
32
|
-
const delayMs = 20;
|
|
33
|
-
let lastError;
|
|
34
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
35
|
-
try {
|
|
36
|
-
return lockfile.lockSync(path, { realpath: false });
|
|
37
|
-
}
|
|
38
|
-
catch (error) {
|
|
39
|
-
const code = typeof error === "object" && error !== null && "code" in error
|
|
40
|
-
? String(error.code)
|
|
41
|
-
: undefined;
|
|
42
|
-
if (code !== "ELOCKED" || attempt === maxAttempts) {
|
|
43
|
-
throw error;
|
|
44
|
-
}
|
|
45
|
-
lastError = error;
|
|
46
|
-
const start = Date.now();
|
|
47
|
-
while (Date.now() - start < delayMs) {
|
|
48
|
-
// busy-wait; synchronous by design
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
throw lastError ?? new Error("Failed to acquire settings lock");
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Atomically read-modify-write the global `settings.json` under the
|
|
56
|
-
* `"observational-memory"` namespace.
|
|
57
|
-
*/
|
|
58
11
|
function withObservationalMemoryLock(fn) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const fileExists = existsSync(path);
|
|
64
|
-
if (fileExists) {
|
|
65
|
-
release = acquireLockSyncWithRetry(path);
|
|
66
|
-
}
|
|
67
|
-
const raw = fileExists ? readFileSync(path, "utf-8") : "{}";
|
|
68
|
-
const parsed = JSON.parse(raw);
|
|
69
|
-
const ns = (parsed["observational-memory"] ?? {});
|
|
70
|
-
const next = fn(ns);
|
|
71
|
-
if (next !== undefined) {
|
|
72
|
-
parsed["observational-memory"] = { ...ns, ...next };
|
|
73
|
-
if (!existsSync(dir)) {
|
|
74
|
-
mkdirSync(dir, { recursive: true });
|
|
75
|
-
}
|
|
76
|
-
if (!release) {
|
|
77
|
-
release = acquireLockSyncWithRetry(path);
|
|
78
|
-
}
|
|
79
|
-
writeFileSync(path, JSON.stringify(parsed, null, 2), "utf-8");
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
finally {
|
|
83
|
-
if (release) {
|
|
84
|
-
release();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
12
|
+
withSettingsLock({ namespace: "observational-memory" }, (current) => {
|
|
13
|
+
const next = fn(current);
|
|
14
|
+
return next;
|
|
15
|
+
});
|
|
87
16
|
}
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// GET handler
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
17
|
export function handleGetSettings(cwd) {
|
|
92
18
|
const config = loadConfig(cwd);
|
|
93
19
|
return {
|
|
@@ -98,16 +24,11 @@ export function handleGetSettings(cwd) {
|
|
|
98
24
|
debugLog: config.debugLog,
|
|
99
25
|
};
|
|
100
26
|
}
|
|
101
|
-
// ---------------------------------------------------------------------------
|
|
102
|
-
// PUT handler
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
27
|
export function handlePutSettings(cwd, body) {
|
|
105
|
-
// Validate key
|
|
106
28
|
if (typeof body.key !== "string" || !ALLOWED_KEYS.has(body.key)) {
|
|
107
29
|
throw new BadRequestError(`settings key must be one of: ${[...ALLOWED_KEYS].join(", ")}`);
|
|
108
30
|
}
|
|
109
31
|
const key = body.key;
|
|
110
|
-
// Validate value
|
|
111
32
|
if (typeof body.value !== "number" || !Number.isInteger(body.value)) {
|
|
112
33
|
throw new BadRequestError("settings value must be an integer");
|
|
113
34
|
}
|
|
@@ -115,7 +36,6 @@ export function handlePutSettings(cwd, body) {
|
|
|
115
36
|
throw new BadRequestError(`token threshold must be between ${MIN_TOKENS.toLocaleString()} and ${MAX_TOKENS.toLocaleString()}`);
|
|
116
37
|
}
|
|
117
38
|
const value = body.value;
|
|
118
|
-
// Persist
|
|
119
39
|
withObservationalMemoryLock((current) => {
|
|
120
40
|
return { ...current, [key]: value };
|
|
121
41
|
});
|