@aiwg/cli 2026.10.4 → 2026.10.6
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/agentic/code/addons/composition-engine/lib/runtime.mjs +1401 -0
- package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
- package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
- package/agentic/code/providers/model-capabilities.v1.json +74 -50
- package/bin/aiwg.mjs +3 -1
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/citation-parser.js +8 -1
- package/dist/src/artifacts/cli.js +72 -9
- package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
- package/dist/src/artifacts/fortemi-core-sync.js +26 -8
- package/dist/src/artifacts/graph-query.js +18 -8
- package/dist/src/artifacts/index-adapters.js +339 -0
- package/dist/src/artifacts/index-builder.js +146 -16
- package/dist/src/artifacts/index-files.js +15 -4
- package/dist/src/artifacts/index-preparation.js +91 -26
- package/dist/src/artifacts/index-reader.js +44 -0
- package/dist/src/artifacts/index-refresh.js +130 -56
- package/dist/src/artifacts/index-status.js +20 -5
- package/dist/src/artifacts/index-worker.mjs +9 -7
- package/dist/src/artifacts/memory-maintenance.js +503 -0
- package/dist/src/artifacts/prebuilt-relations.js +3 -1
- package/dist/src/artifacts/query-engine.js +263 -108
- package/dist/src/artifacts/relations-builder.js +2 -2
- package/dist/src/artifacts/source-graph.js +2 -2
- package/dist/src/artifacts/types.js +9 -1
- package/dist/src/cli/handlers/decision.js +19 -1
- package/dist/src/cli/handlers/guided-setup.js +470 -0
- package/dist/src/cli/handlers/help.js +2 -2
- package/dist/src/cli/handlers/housekeeper.js +1110 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/init.js +41 -6
- package/dist/src/cli/handlers/local-executor.js +5 -3
- package/dist/src/cli/handlers/mc.js +509 -53
- package/dist/src/cli/handlers/models.js +32 -2
- package/dist/src/cli/handlers/ralph-launcher.js +76 -4
- package/dist/src/cli/handlers/rules.js +1 -1
- package/dist/src/cli/handlers/script-runner.js +73 -2
- package/dist/src/cli/handlers/serve.js +5 -3
- package/dist/src/cli/handlers/sessions.js +40 -6
- package/dist/src/cli/handlers/setup.js +428 -1
- package/dist/src/cli/handlers/skill-lint.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +118 -8
- package/dist/src/cli/handlers/use.js +279 -93
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/prompt-utils.js +8 -0
- package/dist/src/cli/router.js +16 -1
- package/dist/src/config/aiwg-config.js +42 -0
- package/dist/src/config/cli.js +7 -3
- package/dist/src/config/decision-config.js +14 -1
- package/dist/src/config/native-sandbox-warnings.js +25 -0
- package/dist/src/contributors/validation.js +6 -5
- package/dist/src/decision/admission.js +7 -3
- package/dist/src/decision/benchmark.js +469 -0
- package/dist/src/decision/ledger.js +22 -3
- package/dist/src/decision/pricing.js +147 -0
- package/dist/src/decision/qualification/gate-evidence.js +18 -15
- package/dist/src/decision/qualification/load-staged.js +539 -0
- package/dist/src/decision/qualification/load.js +109 -33
- package/dist/src/decision/qualification/runner.js +5 -2
- package/dist/src/decision/review/service.js +6 -4
- package/dist/src/decision/review/validate.js +23 -2
- package/dist/src/decision/savings.js +8 -3
- package/dist/src/discovery/index.js +492 -0
- package/dist/src/discovery/interview.js +233 -0
- package/dist/src/discovery/plan-apply.js +571 -0
- package/dist/src/discovery/plan-store.js +171 -0
- package/dist/src/discovery/plan-types.js +2 -0
- package/dist/src/discovery/plan-validation.js +139 -0
- package/dist/src/discovery/types.js +2 -0
- package/dist/src/extensions/commands/definitions.js +49 -9
- package/dist/src/extensions/manifest.js +71 -44
- package/dist/src/extensions/project-local-discovery.js +1 -1
- package/dist/src/extensions/project-quickref.js +18 -17
- package/dist/src/extensions/validation.js +32 -29
- package/dist/src/housekeeper/daemon-config.js +148 -0
- package/dist/src/housekeeper/ipc.js +84 -0
- package/dist/src/housekeeper/lifecycle.js +148 -0
- package/dist/src/housekeeper/platform.js +35 -0
- package/dist/src/housekeeper/status.js +70 -0
- package/dist/src/housekeeper/units.js +114 -0
- package/dist/src/jobs/flow.js +22 -21
- package/dist/src/lint/rule-activation.js +20 -5
- package/dist/src/lint/runner.js +1 -1
- package/dist/src/mcp/cli.mjs +5 -6
- package/dist/src/mcp/config-file.mjs +6 -2
- package/dist/src/mcp/config-layers.mjs +23 -7
- package/dist/src/mcp/profiles.js +18 -31
- package/dist/src/mcp/profiles.mjs +18 -32
- package/dist/src/mcp/registry.js +7 -27
- package/dist/src/mcp/registry.mjs +7 -28
- package/dist/src/mcp/server.mjs +34 -1
- package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
- package/dist/src/mcp/tools/command-run.mjs +3 -2
- package/dist/src/mcp/tools/decision.mjs +49 -48
- package/dist/src/mcp/tools/discovery.mjs +46 -21
- package/dist/src/mcp/tools/orchestration.mjs +22 -22
- package/dist/src/mcp/tools/subsystems.mjs +9 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +17 -4
- package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
- package/dist/src/memory/audit.js +719 -0
- package/dist/src/memory/canonical-context.js +392 -13
- package/dist/src/memory/canonical-maintenance.js +95 -0
- package/dist/src/memory/cli-result.js +22 -0
- package/dist/src/memory/cli.js +176 -0
- package/dist/src/memory/context-pack.js +57 -6
- package/dist/src/memory/cost.js +512 -0
- package/dist/src/memory/firewall-classification.d.mts +7 -0
- package/dist/src/memory/firewall-classification.mjs +32 -0
- package/dist/src/memory/index.js +8 -0
- package/dist/src/memory/line-maintenance.js +68 -0
- package/dist/src/memory/maintenance-cli.js +286 -0
- package/dist/src/memory/maintenance-contract.js +136 -0
- package/dist/src/memory/maintenance-index.js +18 -0
- package/dist/src/memory/maintenance-owner.js +2 -0
- package/dist/src/memory/maintenance-record.js +138 -0
- package/dist/src/memory/maintenance.js +254 -0
- package/dist/src/memory/provider-memory-adapter.js +154 -0
- package/dist/src/memory/recommend-cli.js +83 -0
- package/dist/src/memory/recommend.js +269 -0
- package/dist/src/memory/scheduled.js +81 -0
- package/dist/src/memory/taxonomy.js +44 -0
- package/dist/src/mission-protocol/codecs.js +19 -9
- package/dist/src/models/config-loader.js +11 -1
- package/dist/src/models/model-capabilities.v1.json +74 -50
- package/dist/src/models/provider-policy.js +125 -62
- package/dist/src/models/resolver.js +45 -8
- package/dist/src/models/worker-installation.d.mts +7 -0
- package/dist/src/models/worker-installation.mjs +169 -37
- package/dist/src/models/wrapper-route.js +1 -1
- package/dist/src/output-modes/registry.js +8 -7
- package/dist/src/plugin/metadata-validator.js +1 -1
- package/dist/src/providers/deploy-version.d.mts +1 -0
- package/dist/src/providers/deploy-version.mjs +17 -0
- package/dist/src/providers/provider-definitions.js +19 -16
- package/dist/src/providers/skill-ownership.d.mts +11 -2
- package/dist/src/providers/skill-ownership.mjs +35 -17
- package/dist/src/rules/activation.d.mts +11 -2
- package/dist/src/rules/activation.mjs +82 -6
- package/dist/src/rules/inventory.js +1 -1
- package/dist/src/rules/rule-frontmatter.schema.json +2 -0
- package/dist/src/schema/zod-validation.d.mts +6 -0
- package/dist/src/schema/zod-validation.mjs +50 -0
- package/dist/src/serve/executor-registry.js +297 -20
- package/dist/src/serve/executor-token.d.mts +1 -0
- package/dist/src/serve/executor-token.mjs +8 -0
- package/dist/src/serve/shared-host-scheduler.js +1102 -31
- package/dist/src/serve/worker-invocation.js +114 -15
- package/dist/src/sessions/adapters/claude.js +19 -18
- package/dist/src/sessions/adapters/codex.js +33 -19
- package/dist/src/sessions/adapters/copilot.js +14 -13
- package/dist/src/sessions/adapters/cursor.js +19 -18
- package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
- package/dist/src/sessions/adapters/factory.js +11 -10
- package/dist/src/sessions/adapters/generic.js +32 -23
- package/dist/src/sessions/adapters/grokbot.js +35 -35
- package/dist/src/sessions/adapters/hermes.js +20 -19
- package/dist/src/sessions/adapters/muse.js +50 -49
- package/dist/src/sessions/adapters/omp.js +5 -4
- package/dist/src/sessions/adapters/openclaw.js +30 -29
- package/dist/src/sessions/adapters/opencode.js +26 -26
- package/dist/src/sessions/adapters/openhuman.js +24 -23
- package/dist/src/sessions/adapters/pi.js +5 -4
- package/dist/src/sessions/adapters/windsurf.js +8 -8
- package/dist/src/sessions/analytics.js +10 -9
- package/dist/src/sessions/batch-contracts.js +1 -1
- package/dist/src/sessions/batch-import.js +202 -2
- package/dist/src/sessions/candidates.js +7 -6
- package/dist/src/sessions/contracts.js +73 -54
- package/dist/src/sessions/coverage-preflight.js +26 -1
- package/dist/src/sessions/output-registration.js +11 -10
- package/dist/src/sessions/policy.js +18 -7
- package/dist/src/sessions/refresh.js +2 -3
- package/dist/src/sessions/repository.js +4 -2
- package/dist/src/sessions/workspace-discovery.js +92 -26
- package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
- package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
- package/dist/src/update/checker.d.mts +4 -0
- package/dist/src/update/checker.mjs +14 -0
- package/dist/src/writing/exemplar-selection.js +6 -6
- package/dist/src/writing/voice-evaluation.js +24 -23
- package/dist/src/writing/writer-migration.js +14 -13
- package/dist/src/writing/writer-profile.js +19 -18
- package/dist/src/writing/writing-brief.js +18 -17
- package/dist/src/writing/writing-receipt.js +47 -46
- package/package.json +5 -2
- package/schemas/decision/DecisionReview.schema.json +78 -1
- package/schemas/mission-v1.schema.json +3 -0
- package/tools/agents/deploy-agents.mjs +67 -39
- package/tools/agents/providers/antigravity.mjs +6 -1
- package/tools/agents/providers/base.mjs +19 -13
- package/tools/agents/providers/claude.mjs +1 -1
- package/tools/agents/providers/codex.mjs +50 -56
- package/tools/agents/providers/copilot.mjs +8 -2
- package/tools/agents/providers/cursor.mjs +1 -1
- package/tools/agents/providers/deepseek-harness.mjs +1 -1
- package/tools/agents/providers/factory.mjs +7 -1
- package/tools/agents/providers/grok-build.mjs +18 -2
- package/tools/agents/providers/grokbot.mjs +1 -1
- package/tools/agents/providers/hermes.mjs +7 -13
- package/tools/agents/providers/muse-hooks.mjs +21 -14
- package/tools/agents/providers/muse.mjs +7 -2
- package/tools/agents/providers/omp.mjs +28 -4
- package/tools/agents/providers/opencode.mjs +14 -5
- package/tools/agents/providers/openhuman.mjs +1 -1
- package/tools/agents/providers/pi.mjs +1 -1
- package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
- package/tools/agents/providers/rule-delivery.mjs +263 -71
- package/tools/agents/providers/windsurf.mjs +7 -0
- package/tools/cli/workspace-status.mjs +5 -0
- package/tools/commands/deploy-prompts-codex.mjs +11 -8
- package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
- package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
- package/tools/ralph-external/process-monitor.mjs +7 -4
- package/tools/ralph-external/session-launcher.mjs +280 -32
- package/tools/skills/deploy-skills-codex.mjs +11 -7
- package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal JSON-RPC 2.0 client for the housekeeper daemon socket
|
|
3
|
+
* (epic #3353 slice B5).
|
|
4
|
+
*
|
|
5
|
+
* Speaks the same newline-delimited JSON framing as
|
|
6
|
+
* `tools/daemon/ipc-server.mjs` without importing `tools/` — `src/` must
|
|
7
|
+
* never import `tools/` (tools import compiled src via `importImpl`).
|
|
8
|
+
*/
|
|
9
|
+
import net from 'node:net';
|
|
10
|
+
export async function requestDaemon(socketPath, method, options = {}) {
|
|
11
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let settled = false;
|
|
14
|
+
const fail = (error) => {
|
|
15
|
+
if (settled)
|
|
16
|
+
return;
|
|
17
|
+
settled = true;
|
|
18
|
+
try {
|
|
19
|
+
socket.destroy();
|
|
20
|
+
}
|
|
21
|
+
catch { /* already closed */ }
|
|
22
|
+
reject(error);
|
|
23
|
+
};
|
|
24
|
+
const succeed = (value) => {
|
|
25
|
+
if (settled)
|
|
26
|
+
return;
|
|
27
|
+
settled = true;
|
|
28
|
+
try {
|
|
29
|
+
socket.end();
|
|
30
|
+
}
|
|
31
|
+
catch { /* already closed */ }
|
|
32
|
+
resolve(value);
|
|
33
|
+
};
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
fail(new Error(`Daemon request timed out: ${method} (${timeoutMs}ms)`));
|
|
36
|
+
}, timeoutMs);
|
|
37
|
+
timer.unref?.();
|
|
38
|
+
let buffer = '';
|
|
39
|
+
const socket = net.createConnection(socketPath);
|
|
40
|
+
socket.once('connect', () => {
|
|
41
|
+
socket.write(`${JSON.stringify({
|
|
42
|
+
jsonrpc: '2.0', id: 1, method, params: options.params ?? {},
|
|
43
|
+
})}\n`);
|
|
44
|
+
});
|
|
45
|
+
socket.on('data', (chunk) => {
|
|
46
|
+
buffer += chunk.toString('utf8');
|
|
47
|
+
const newline = buffer.indexOf('\n');
|
|
48
|
+
if (newline === -1)
|
|
49
|
+
return;
|
|
50
|
+
const line = buffer.slice(0, newline).trim();
|
|
51
|
+
buffer = buffer.slice(newline + 1);
|
|
52
|
+
if (!line)
|
|
53
|
+
return;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
let response;
|
|
56
|
+
try {
|
|
57
|
+
response = JSON.parse(line);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
fail(new Error(`Daemon returned invalid JSON for ${method}`));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (response.error) {
|
|
64
|
+
fail(new Error(`Daemon error for ${method}: ${response.error.message ?? 'unknown error'}`));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
succeed(response.result);
|
|
68
|
+
});
|
|
69
|
+
socket.once('error', (error) => {
|
|
70
|
+
clearTimeout(timer);
|
|
71
|
+
const code = error?.code;
|
|
72
|
+
if (code === 'ENOENT' || code === 'ECONNREFUSED') {
|
|
73
|
+
fail(new Error('Daemon is not running (socket unavailable)'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
fail(error);
|
|
77
|
+
});
|
|
78
|
+
socket.once('close', () => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
fail(new Error(`Daemon connection closed during ${method}`));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=ipc.js.map
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Housekeeper daemon lifecycle helpers (epic #3353 slice B5).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the pid/lock/heartbeat/state/socket paths of
|
|
5
|
+
* `tools/daemon/daemon-main.mjs` so `aiwg housekeeper start|stop|status`
|
|
6
|
+
* serializes against the same single-instance lock: a second `start`
|
|
7
|
+
* fails clearly, and `stop` cleans up exactly the files the daemon owns.
|
|
8
|
+
* All process signalling of owned ChildProcess handles goes through
|
|
9
|
+
* `signalChild`; pid-based stop uses `process.kill` (no ChildProcess).
|
|
10
|
+
*/
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { signalChild } from '../process/signal-child.mjs';
|
|
14
|
+
/** Same relative layout as DaemonMain; resolved against the caller cwd. */
|
|
15
|
+
export function daemonPaths(cwd, frameworkRoot) {
|
|
16
|
+
return {
|
|
17
|
+
pidFile: path.join(cwd, '.aiwg', 'daemon.pid'),
|
|
18
|
+
lockFile: path.join(cwd, '.aiwg', 'daemon', '.lock'),
|
|
19
|
+
heartbeatFile: path.join(cwd, '.aiwg', 'daemon', 'heartbeat'),
|
|
20
|
+
stateFile: path.join(cwd, '.aiwg', 'daemon', 'state.json'),
|
|
21
|
+
socketPath: path.join(cwd, '.aiwg', 'daemon', 'daemon.sock'),
|
|
22
|
+
logFile: path.join(cwd, '.aiwg', 'daemon', 'daemon.log'),
|
|
23
|
+
taskStorePath: path.join(cwd, '.aiwg', 'daemon', 'tasks.json'),
|
|
24
|
+
daemonScript: path.join(frameworkRoot, 'tools', 'daemon', 'daemon-main.mjs'),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function readPidFile(pidFile) {
|
|
28
|
+
let raw;
|
|
29
|
+
try {
|
|
30
|
+
raw = fs.readFileSync(pidFile, 'utf8').trim();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const pid = Number.parseInt(raw, 10);
|
|
36
|
+
return Number.isSafeInteger(pid) && pid > 0 && String(pid) === raw ? pid : null;
|
|
37
|
+
}
|
|
38
|
+
export function defaultKill(pid, signal) {
|
|
39
|
+
process.kill(pid, signal);
|
|
40
|
+
}
|
|
41
|
+
export function isPidAlive(pid) {
|
|
42
|
+
try {
|
|
43
|
+
process.kill(pid, 0);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// ESRCH = no such process. Any other error (e.g. EPERM) means a
|
|
48
|
+
// process exists that we may not signal — treat as alive.
|
|
49
|
+
if (error?.code === 'ESRCH')
|
|
50
|
+
return false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Report whether a daemon owns the pid file. A pid file pointing at a
|
|
56
|
+
* dead process is stale (safe to overwrite after cleanup); a live one
|
|
57
|
+
* blocks `start`.
|
|
58
|
+
*/
|
|
59
|
+
export function describeRunning(pidFile) {
|
|
60
|
+
const pid = readPidFile(pidFile);
|
|
61
|
+
if (pid === null)
|
|
62
|
+
return { running: false, pid: null, stale: false };
|
|
63
|
+
if (isPidAlive(pid))
|
|
64
|
+
return { running: true, pid, stale: false };
|
|
65
|
+
return { running: false, pid, stale: true };
|
|
66
|
+
}
|
|
67
|
+
export function removeLifecycleFiles(paths) {
|
|
68
|
+
for (const file of [paths.pidFile, paths.lockFile]) {
|
|
69
|
+
try {
|
|
70
|
+
fs.unlinkSync(file);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error?.code !== 'ENOENT')
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const defaultSleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); });
|
|
79
|
+
/**
|
|
80
|
+
* Stop the daemon gracefully (SIGTERM, poll, then SIGKILL on timeout),
|
|
81
|
+
* then release the pid/lock files. Never throws for absent files.
|
|
82
|
+
*/
|
|
83
|
+
export async function stopDaemon(options) {
|
|
84
|
+
const { paths, kill = defaultKill, timeoutMs = 10_000, pollMs = 200 } = options;
|
|
85
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
86
|
+
const state = describeRunning(paths.pidFile);
|
|
87
|
+
if (state.pid === null)
|
|
88
|
+
return { outcome: 'not-running' };
|
|
89
|
+
if (!state.running) {
|
|
90
|
+
removeLifecycleFiles(paths);
|
|
91
|
+
return { outcome: 'stale-cleaned', pid: state.pid };
|
|
92
|
+
}
|
|
93
|
+
const pid = state.pid;
|
|
94
|
+
try {
|
|
95
|
+
kill(pid, 'SIGTERM');
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error?.code === 'ESRCH') {
|
|
99
|
+
removeLifecycleFiles(paths);
|
|
100
|
+
return { outcome: 'stale-cleaned', pid };
|
|
101
|
+
}
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
const deadline = Date.now() + timeoutMs;
|
|
105
|
+
while (isPidAlive(pid)) {
|
|
106
|
+
if (Date.now() >= deadline)
|
|
107
|
+
break;
|
|
108
|
+
await sleep(pollMs);
|
|
109
|
+
}
|
|
110
|
+
let forced = false;
|
|
111
|
+
if (isPidAlive(pid)) {
|
|
112
|
+
try {
|
|
113
|
+
kill(pid, 'SIGKILL');
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
if (error?.code !== 'ESRCH')
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
forced = true;
|
|
120
|
+
const killDeadline = Date.now() + Math.min(timeoutMs, 5000);
|
|
121
|
+
while (isPidAlive(pid) && Date.now() < killDeadline) {
|
|
122
|
+
await sleep(pollMs);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
removeLifecycleFiles(paths);
|
|
126
|
+
return { outcome: 'stopped', pid, forced };
|
|
127
|
+
}
|
|
128
|
+
/** Forward a terminal signal to an owned foreground child. */
|
|
129
|
+
export function forwardSignalToChild(child, signal) {
|
|
130
|
+
signalChild(child, signal);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Poll until `predicate` is true or the deadline passes. Returns true on
|
|
134
|
+
* success; used by `start` to wait for the pid file without fixed sleeps.
|
|
135
|
+
*/
|
|
136
|
+
export async function pollUntil(predicate, options = {}) {
|
|
137
|
+
const { timeoutMs = 10_000, pollMs = 200 } = options;
|
|
138
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
139
|
+
const deadline = Date.now() + timeoutMs;
|
|
140
|
+
for (;;) {
|
|
141
|
+
if (predicate())
|
|
142
|
+
return true;
|
|
143
|
+
if (Date.now() >= deadline)
|
|
144
|
+
return false;
|
|
145
|
+
await sleep(pollMs);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
//# sourceMappingURL=lifecycle.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform honesty for `aiwg housekeeper` (epic #3353 slice B5).
|
|
3
|
+
*
|
|
4
|
+
* Design-note §3 matrix in one place, with the platform injectable so
|
|
5
|
+
* tests can cover every row without running on that OS: Windows is
|
|
6
|
+
* unsupported (WSL2 follows the Linux rows); macOS install is a
|
|
7
|
+
* best-effort launchd plist (documented, not CI-tested); Linux install
|
|
8
|
+
* targets systemd user units.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizePlatform(platform = process.platform) {
|
|
11
|
+
if (platform === 'win32')
|
|
12
|
+
return 'windows';
|
|
13
|
+
if (platform === 'darwin')
|
|
14
|
+
return 'darwin';
|
|
15
|
+
if (platform === 'linux')
|
|
16
|
+
return 'linux';
|
|
17
|
+
return 'other';
|
|
18
|
+
}
|
|
19
|
+
/** run-once / daemon processes: everything except Windows. */
|
|
20
|
+
export function runSupported(platform = process.platform) {
|
|
21
|
+
return normalizePlatform(platform) !== 'windows';
|
|
22
|
+
}
|
|
23
|
+
export function runUnsupportedMessage() {
|
|
24
|
+
return 'aiwg housekeeper is not supported on Windows (no recursive fs.watch, POSIX paths assumed). Use WSL2 and follow the Linux rows.';
|
|
25
|
+
}
|
|
26
|
+
/** Install backends offered on a platform, in preference order. */
|
|
27
|
+
export function installBackends(platform = process.platform) {
|
|
28
|
+
const normalized = normalizePlatform(platform);
|
|
29
|
+
if (normalized === 'windows')
|
|
30
|
+
return [];
|
|
31
|
+
if (normalized === 'darwin')
|
|
32
|
+
return ['launchd', 'systemd-user'];
|
|
33
|
+
return ['systemd-user', 'launchd'];
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=platform.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Housekeeper status envelope (epic #3353 slice B5).
|
|
3
|
+
*
|
|
4
|
+
* `aiwg housekeeper status --json` emits this shape: the B1 §6 contract
|
|
5
|
+
* fields (`contractVersion`/`command`/`status`/`mode`/`jobs`/`totals`) plus
|
|
6
|
+
* per-index freshness measured by the B4 adapters. The CLI builds it from
|
|
7
|
+
* live daemon IPC when a daemon answers, otherwise from local config and
|
|
8
|
+
* persisted receipts (offline mode), so ordinary `aiwg` commands keep
|
|
9
|
+
* working with no daemon running.
|
|
10
|
+
*/
|
|
11
|
+
export const HOUSEKEEPER_STATUS_CONTRACT_VERSION = 1;
|
|
12
|
+
const RECEIPT_STATES = new Set(['ok', 'failed', 'skipped', 'deferred']);
|
|
13
|
+
export function countJobTotals(jobs) {
|
|
14
|
+
const totals = {
|
|
15
|
+
jobs: jobs.length, ok: 0, failed: 0, skipped: 0, deferred: 0, never: 0,
|
|
16
|
+
};
|
|
17
|
+
for (const job of jobs) {
|
|
18
|
+
if (job.lastState === 'ok')
|
|
19
|
+
totals.ok += 1;
|
|
20
|
+
else if (job.lastState === 'failed')
|
|
21
|
+
totals.failed += 1;
|
|
22
|
+
else if (job.lastState === 'skipped')
|
|
23
|
+
totals.skipped += 1;
|
|
24
|
+
else if (job.lastState === 'deferred')
|
|
25
|
+
totals.deferred += 1;
|
|
26
|
+
else
|
|
27
|
+
totals.never += 1;
|
|
28
|
+
}
|
|
29
|
+
return totals;
|
|
30
|
+
}
|
|
31
|
+
export function normalizeJobState(state) {
|
|
32
|
+
return typeof state === 'string' && RECEIPT_STATES.has(state)
|
|
33
|
+
? state
|
|
34
|
+
: 'never';
|
|
35
|
+
}
|
|
36
|
+
/** Assemble the envelope; every field is always present. */
|
|
37
|
+
export function buildHousekeeperStatus(input) {
|
|
38
|
+
const jobs = input.jobs ?? [];
|
|
39
|
+
return {
|
|
40
|
+
contractVersion: HOUSEKEEPER_STATUS_CONTRACT_VERSION,
|
|
41
|
+
command: 'housekeeper.status',
|
|
42
|
+
status: 'ok',
|
|
43
|
+
mode: input.mode,
|
|
44
|
+
pid: input.pid ?? null,
|
|
45
|
+
uptimeSeconds: input.uptimeSeconds ?? 0,
|
|
46
|
+
jobs,
|
|
47
|
+
totals: countJobTotals(jobs),
|
|
48
|
+
freshness: input.freshness ?? [],
|
|
49
|
+
update: {
|
|
50
|
+
channel: 'stable',
|
|
51
|
+
checkOnly: true,
|
|
52
|
+
lastCheck: input.lastCheck ?? null,
|
|
53
|
+
available: input.available ?? null,
|
|
54
|
+
lastInstall: input.lastInstall ?? null,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Map a B4 freshness snapshot to the status entry. `unavailable` (no local
|
|
60
|
+
* deterministic probe) surfaces as `unsupported` per the B1 §6 state set.
|
|
61
|
+
*/
|
|
62
|
+
export function toFreshnessEntry(snapshot) {
|
|
63
|
+
return {
|
|
64
|
+
index: snapshot.name,
|
|
65
|
+
state: snapshot.status === 'unavailable' ? 'unsupported' : snapshot.status,
|
|
66
|
+
targetMs: snapshot.policy?.targetStaleAfterMs ?? null,
|
|
67
|
+
converged: snapshot.convergence?.converged ?? null,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autostart unit renderers for `aiwg housekeeper install` (epic #3353 B5).
|
|
3
|
+
*
|
|
4
|
+
* Platform matrix (design note §3): Linux + systemd is documented;
|
|
5
|
+
* Linux without systemd and macOS fall back to foreground `start` or an
|
|
6
|
+
* external timer calling `run-once`; Windows is unsupported (WSL2 follows
|
|
7
|
+
* the Linux rows). Units are pure strings here so `--dry-run` can print
|
|
8
|
+
* them without writing and tests can assert content with an injected
|
|
9
|
+
* platform. This module never enables anything: enabling is an explicit
|
|
10
|
+
* operator step printed by the handler, never performed implicitly.
|
|
11
|
+
*/
|
|
12
|
+
import os from 'node:os';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
export const HOUSEKEEPER_SERVICE_NAME = 'aiwg-housekeeper.service';
|
|
15
|
+
export const HOUSEKEEPER_TIMER_NAME = 'aiwg-housekeeper.timer';
|
|
16
|
+
export const HOUSEKEEPER_LAUNCHD_LABEL = 'dev.aiwg.housekeeper';
|
|
17
|
+
export function renderSystemdService(ctx) {
|
|
18
|
+
const node = ctx.nodeBin ?? process.execPath;
|
|
19
|
+
const runOnce = ctx.jobId
|
|
20
|
+
? `${quote(ctx.aiwgBin)} housekeeper run-once --job ${quote(ctx.jobId)}`
|
|
21
|
+
: `${quote(ctx.aiwgBin)} housekeeper run-once`;
|
|
22
|
+
return [
|
|
23
|
+
'[Unit]',
|
|
24
|
+
'Description=AIWG housekeeper (deterministic maintenance, oneshot for timer)',
|
|
25
|
+
'Documentation=https://github.com/jmagly/aiwg',
|
|
26
|
+
'',
|
|
27
|
+
'[Service]',
|
|
28
|
+
'Type=oneshot',
|
|
29
|
+
`WorkingDirectory=${ctx.cwd}`,
|
|
30
|
+
`ExecStart=${node} ${runOnce}`,
|
|
31
|
+
'NoNewPrivileges=true',
|
|
32
|
+
'',
|
|
33
|
+
'[Install]',
|
|
34
|
+
'WantedBy=default.target',
|
|
35
|
+
'',
|
|
36
|
+
].join('\n');
|
|
37
|
+
}
|
|
38
|
+
export function renderSystemdTimer(options = {}) {
|
|
39
|
+
return [
|
|
40
|
+
'[Unit]',
|
|
41
|
+
'Description=AIWG housekeeper periodic run (calls aiwg housekeeper run-once)',
|
|
42
|
+
'',
|
|
43
|
+
'[Timer]',
|
|
44
|
+
`OnCalendar=${options.calendar ?? 'hourly'}`,
|
|
45
|
+
'Persistent=true',
|
|
46
|
+
`Unit=${HOUSEKEEPER_SERVICE_NAME}`,
|
|
47
|
+
'',
|
|
48
|
+
'[Install]',
|
|
49
|
+
'WantedBy=timers.target',
|
|
50
|
+
'',
|
|
51
|
+
].join('\n');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Best-effort macOS plist (design note §3: documented, not CI-tested —
|
|
55
|
+
* there is no macOS runner). Runs `run-once` on a calendar interval.
|
|
56
|
+
*/
|
|
57
|
+
export function renderLaunchdPlist(ctx, options = {}) {
|
|
58
|
+
const node = ctx.nodeBin ?? process.execPath;
|
|
59
|
+
const args = ctx.jobId
|
|
60
|
+
? [ctx.aiwgBin, 'housekeeper', 'run-once', '--job', ctx.jobId]
|
|
61
|
+
: [ctx.aiwgBin, 'housekeeper', 'run-once'];
|
|
62
|
+
const argsXml = [node, ...args].map((arg) => ` <string>${xmlEscape(arg)}</string>`).join('\n');
|
|
63
|
+
return [
|
|
64
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
65
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
66
|
+
'<plist version="1.0">',
|
|
67
|
+
'<dict>',
|
|
68
|
+
' <key>Label</key>',
|
|
69
|
+
` <string>${HOUSEKEEPER_LAUNCHD_LABEL}</string>`,
|
|
70
|
+
' <key>ProgramArguments</key>',
|
|
71
|
+
' <array>',
|
|
72
|
+
argsXml,
|
|
73
|
+
' </array>',
|
|
74
|
+
' <key>WorkingDirectory</key>',
|
|
75
|
+
` <string>${xmlEscape(ctx.cwd)}</string>`,
|
|
76
|
+
' <key>StartInterval</key>',
|
|
77
|
+
` <integer>${options.intervalSeconds ?? 3600}</integer>`,
|
|
78
|
+
' <key>RunAtLoad</key>',
|
|
79
|
+
' <false/>',
|
|
80
|
+
'</dict>',
|
|
81
|
+
'</plist>',
|
|
82
|
+
'',
|
|
83
|
+
].join('\n');
|
|
84
|
+
}
|
|
85
|
+
/** Default `aiwg` entry point under a framework root (overridable in tests). */
|
|
86
|
+
export function defaultAiwgBin(frameworkRoot) {
|
|
87
|
+
return path.join(frameworkRoot, 'bin', 'aiwg.mjs');
|
|
88
|
+
}
|
|
89
|
+
/** XDG user systemd directory (`~/.config/systemd/user` by default). */
|
|
90
|
+
export function systemdUserDir(home = os.homedir()) {
|
|
91
|
+
const base = process.env['XDG_CONFIG_HOME'] || path.join(home, '.config');
|
|
92
|
+
return path.join(base, 'systemd', 'user');
|
|
93
|
+
}
|
|
94
|
+
/** macOS user agent directory. */
|
|
95
|
+
export function launchdAgentDir(home = os.homedir()) {
|
|
96
|
+
return path.join(home, 'Library', 'LaunchAgents');
|
|
97
|
+
}
|
|
98
|
+
export function backendForPlatform(platform) {
|
|
99
|
+
if (platform === 'win32')
|
|
100
|
+
return 'unsupported';
|
|
101
|
+
if (platform === 'darwin')
|
|
102
|
+
return 'launchd';
|
|
103
|
+
return 'systemd-user';
|
|
104
|
+
}
|
|
105
|
+
function quote(value) {
|
|
106
|
+
return /[\s"']/.test(value) ? `"${value.replace(/"/g, '\\"')}"` : value;
|
|
107
|
+
}
|
|
108
|
+
function xmlEscape(value) {
|
|
109
|
+
return value
|
|
110
|
+
.replace(/&/g, '&')
|
|
111
|
+
.replace(/</g, '<')
|
|
112
|
+
.replace(/>/g, '>');
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=units.js.map
|
package/dist/src/jobs/flow.js
CHANGED
|
@@ -2,6 +2,7 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { load as loadYaml } from 'js-yaml';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
+
import { integer, url } from '../schema/zod-validation.mjs';
|
|
5
6
|
import { JOB_API_VERSION, JOB_KIND } from './types.js';
|
|
6
7
|
const relativeFile = z.string().min(1).refine(value => {
|
|
7
8
|
if (path.isAbsolute(value))
|
|
@@ -12,51 +13,51 @@ const relativeFile = z.string().min(1).refine(value => {
|
|
|
12
13
|
const absolutePath = z.string().min(1)
|
|
13
14
|
.refine(path.isAbsolute, 'must be an absolute path')
|
|
14
15
|
.refine(value => path.resolve(value) !== path.parse(path.resolve(value)).root, 'must not be a filesystem root');
|
|
15
|
-
const httpsOrigin =
|
|
16
|
+
const httpsOrigin = url().refine(value => new URL(value).protocol === 'https:', 'must use https');
|
|
16
17
|
const accountName = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.@:-]{0,127}$/u);
|
|
17
|
-
export const externalJobFlowSchema = z.
|
|
18
|
+
export const externalJobFlowSchema = z.strictObject({
|
|
18
19
|
apiVersion: z.literal(JOB_API_VERSION),
|
|
19
20
|
kind: z.literal(JOB_KIND),
|
|
20
|
-
metadata: z.
|
|
21
|
+
metadata: z.strictObject({
|
|
21
22
|
name: z.string().regex(/^[a-z][a-z0-9-]{0,62}$/u),
|
|
22
23
|
revision: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u),
|
|
23
|
-
})
|
|
24
|
-
spec: z.
|
|
25
|
-
trigger: z.
|
|
26
|
-
executor: z.
|
|
24
|
+
}),
|
|
25
|
+
spec: z.strictObject({
|
|
26
|
+
trigger: z.strictObject({ type: z.literal('external') }),
|
|
27
|
+
executor: z.strictObject({
|
|
27
28
|
provider: z.literal('codex'),
|
|
28
29
|
mode: z.literal('exec'),
|
|
29
30
|
workspace: absolutePath,
|
|
30
31
|
prompt: relativeFile,
|
|
31
32
|
resultSchema: relativeFile,
|
|
32
33
|
binary: absolutePath,
|
|
33
|
-
})
|
|
34
|
-
workItem: z.
|
|
34
|
+
}),
|
|
35
|
+
workItem: z.strictObject({
|
|
35
36
|
provider: z.literal('gitea'),
|
|
36
|
-
baseUrl:
|
|
37
|
+
baseUrl: url().refine(value => new URL(value).protocol === 'https:', 'must use https'),
|
|
37
38
|
repository: z.string().regex(/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u),
|
|
38
39
|
tokenFile: absolutePath,
|
|
39
40
|
eligibleLabels: z.array(z.string().min(1)).min(1),
|
|
40
|
-
claimTtlSeconds:
|
|
41
|
-
claimSettleMs:
|
|
42
|
-
})
|
|
43
|
-
approval: z.
|
|
41
|
+
claimTtlSeconds: integer().min(30).max(86400).optional(),
|
|
42
|
+
claimSettleMs: integer().min(100).max(30000).optional(),
|
|
43
|
+
}),
|
|
44
|
+
approval: z.strictObject({
|
|
44
45
|
required: z.boolean().optional(),
|
|
45
46
|
label: z.string().min(1).optional(),
|
|
46
|
-
}).
|
|
47
|
-
security: z.
|
|
47
|
+
}).optional(),
|
|
48
|
+
security: z.strictObject({
|
|
48
49
|
allowedOrigins: z.array(httpsOrigin).min(1),
|
|
49
50
|
allowedAccounts: z.array(accountName).min(1),
|
|
50
51
|
approvedAttachmentRoots: z.array(absolutePath),
|
|
51
52
|
sensitiveValueFiles: z.array(absolutePath).optional(),
|
|
52
|
-
})
|
|
53
|
-
completion: z.
|
|
53
|
+
}),
|
|
54
|
+
completion: z.strictObject({
|
|
54
55
|
require: z.array(z.enum([
|
|
55
56
|
'external-result-url', 'issue-comment', 'idempotency-key', 'verification',
|
|
56
57
|
])).min(1),
|
|
57
|
-
})
|
|
58
|
-
})
|
|
59
|
-
}).
|
|
58
|
+
}),
|
|
59
|
+
}),
|
|
60
|
+
}).superRefine((flow, ctx) => {
|
|
60
61
|
const workspace = path.resolve(flow.spec.executor.workspace);
|
|
61
62
|
const within = (candidate) => {
|
|
62
63
|
const relative = path.relative(workspace, path.resolve(candidate));
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
/** Activation checks within the existing lint runner, not a delivery selector. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
1
4
|
import { loadLocalCapabilityEntries, resolveRuleBindingTarget } from '../artifacts/query-engine.js';
|
|
2
|
-
import { parseRuleFrontmatter, RULE_BINDING_TYPES } from '../rules/activation.mjs';
|
|
5
|
+
import { parseRuleFrontmatter, RULE_BINDING_TYPES, coreBodyPath, validateCoreBody } from '../rules/activation.mjs';
|
|
3
6
|
export const ruleActivationRuleset = {
|
|
4
7
|
id: 'rule-activation', name: 'Rule activation', framework: 'core', version: '1.0.0',
|
|
5
8
|
description: 'Validate rule and behavior activation contracts and exact indexed bindings',
|
|
@@ -10,17 +13,29 @@ export const ruleActivationRuleset = {
|
|
|
10
13
|
checks: [{ type: 'rule-activation' }],
|
|
11
14
|
}],
|
|
12
15
|
};
|
|
13
|
-
export function lintRuleActivation(content, file, projectRoot, entries) {
|
|
14
|
-
//
|
|
15
|
-
if (/^(?:RULES-INDEX|RULES-ONDEMAND)\.md$/.test(file.split('/').pop()))
|
|
16
|
+
export function lintRuleActivation(content, file, projectRoot, entries, absolutePath) {
|
|
17
|
+
// Directory documentation and generated indexes are not authored rules.
|
|
18
|
+
if (/^(?:README|RULES-INDEX|RULES-ONDEMAND)\.md$/.test(file.split('/').pop()))
|
|
16
19
|
return [];
|
|
20
|
+
// <rule>.core.md is a delivery body (#3450), validated through its owning rule.
|
|
21
|
+
if (/\.core\.md$/.test(file))
|
|
22
|
+
return [];
|
|
23
|
+
const shipped = /(?:^|\/)agentic\/code\//.test((absolutePath ?? path.resolve(projectRoot, file)).replaceAll('\\', '/'));
|
|
17
24
|
const result = parseRuleFrontmatter(content);
|
|
18
25
|
const diagnostics = result.diagnostics.map(item => ({
|
|
19
26
|
ruleId: 'rules/activation', ruleName: 'Rule activation contract',
|
|
20
|
-
file, severity: item.severity, message: item.message,
|
|
27
|
+
file, severity: item.code === 'missing-activation' && shipped ? 'error' : item.severity, message: item.message,
|
|
21
28
|
}));
|
|
22
29
|
if (!result.rule)
|
|
23
30
|
return diagnostics;
|
|
31
|
+
if (result.rule.activation === 'core') {
|
|
32
|
+
const body = coreBodyPath(absolutePath ?? (path.isAbsolute(file) ? file : path.resolve(projectRoot, file)));
|
|
33
|
+
const problems = fs.existsSync(body)
|
|
34
|
+
? validateCoreBody(fs.readFileSync(body, 'utf8').trim(), path.basename(body))
|
|
35
|
+
: [`${path.basename(body)}: activation core requires a compact body file beside the rule`];
|
|
36
|
+
for (const message of problems)
|
|
37
|
+
diagnostics.push({ ruleId: 'rules/activation', ruleName: 'Rule activation contract', file, severity: 'error', message });
|
|
38
|
+
}
|
|
24
39
|
const bindings = result.rule.bound_to;
|
|
25
40
|
if (!Object.values(bindings).some(list => list.length > 0))
|
|
26
41
|
return diagnostics;
|
package/dist/src/lint/runner.js
CHANGED
|
@@ -563,7 +563,7 @@ async function runRule(rule, targetDir, allFiles, referenceIndex, projectRoot, a
|
|
|
563
563
|
if (check.type === 'id-unique')
|
|
564
564
|
continue; // Already handled
|
|
565
565
|
if (check.type === 'rule-activation') {
|
|
566
|
-
diagnostics.push(...lintRuleActivation(content, file, projectRoot, artifactEntries));
|
|
566
|
+
diagnostics.push(...lintRuleActivation(content, file, projectRoot, artifactEntries, absPath));
|
|
567
567
|
continue;
|
|
568
568
|
}
|
|
569
569
|
const checkDiagnostics = runCheck(check, content, frontmatter, file, targetDir, allFiles, referenceIndex, projectRoot);
|
package/dist/src/mcp/cli.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { startServer, createServer } from './server.mjs';
|
|
10
10
|
import { assertConfigDestination, assertConfigObject, assertProjectCredentials, isPathInsideDir, classifyMcpDestination, projectCredentialRemedy, writeConfigAtomic, writeConfigTransaction } from './config-file.mjs';
|
|
11
|
+
import { replaceServer } from './toml-editor.mjs';
|
|
11
12
|
import fs from 'fs/promises';
|
|
12
13
|
import path from 'path';
|
|
13
14
|
import os from 'os';
|
|
@@ -335,16 +336,14 @@ enabled_tools = [
|
|
|
335
336
|
if (error.code !== 'ENOENT') throw error;
|
|
336
337
|
}
|
|
337
338
|
|
|
338
|
-
//
|
|
339
|
-
|
|
339
|
+
// Parse before deciding whether to write, including already configured files.
|
|
340
|
+
const edited = replaceServer(existing, 'aiwg', tomlContent.trim());
|
|
341
|
+
if (edited.alreadyPresent) {
|
|
340
342
|
console.log('AIWG MCP already configured in ~/.codex/config.toml');
|
|
341
343
|
return true;
|
|
342
344
|
}
|
|
343
345
|
|
|
344
|
-
|
|
345
|
-
const updated = existing.trimEnd() + '\n' + tomlContent.trim() + '\n';
|
|
346
|
-
|
|
347
|
-
await writeConfigAtomic(configPath, updated, { userScope: true });
|
|
346
|
+
await writeConfigAtomic(configPath, edited.text, { userScope: true });
|
|
348
347
|
console.log(`MCP configuration appended to: ${configPath}`);
|
|
349
348
|
console.log(`\nTo use AIWG MCP server with Codex:`);
|
|
350
349
|
console.log(` 1. Restart Codex CLI`);
|
|
@@ -187,11 +187,15 @@ export async function writeConfigAtomic(file, content, { userScope = false, proj
|
|
|
187
187
|
if (rejectSymlinkParents) await assertNoSymlinkParents(file, symlinkRoot);
|
|
188
188
|
const info = await assertConfigDestination(file, projectRoot);
|
|
189
189
|
const mode = restoreMode ?? (userScope ? 0o600 : info ? info.mode & 0o7777 : newFileMode ?? 0o666);
|
|
190
|
-
|
|
190
|
+
// Restrict newly created parents for private settings, ownership and output files.
|
|
191
|
+
// Recursive mkdir leaves every existing directory's permissions unchanged.
|
|
192
|
+
await mkdir(dirname(file), { recursive: true, mode: (mode & 0o077) === 0 ? 0o700 : undefined });
|
|
191
193
|
const temporary = `${file}.${randomUUID()}.tmp`;
|
|
192
194
|
let handle;
|
|
195
|
+
let created = false;
|
|
193
196
|
try {
|
|
194
197
|
handle = await open(temporary, 'wx', mode);
|
|
198
|
+
created = true;
|
|
195
199
|
await handle.writeFile(content, 'utf-8');
|
|
196
200
|
if (userScope || info || restoreMode !== undefined || newFileMode !== undefined) await handle.chmod(mode);
|
|
197
201
|
await handle.sync();
|
|
@@ -204,7 +208,7 @@ export async function writeConfigAtomic(file, content, { userScope = false, proj
|
|
|
204
208
|
try {
|
|
205
209
|
if (handle) await handle.close();
|
|
206
210
|
} finally {
|
|
207
|
-
await unlink(temporary).catch(() => {});
|
|
211
|
+
if (created) await unlink(temporary).catch(() => {});
|
|
208
212
|
}
|
|
209
213
|
}
|
|
210
214
|
}
|