@awak-app/simy-cli 0.7.21 → 0.7.22
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/README.md +16 -0
- package/package.json +2 -2
- package/src/cli-contract.js +1 -1
- package/src/desktop-host.js +65 -6
- package/src/sqm/agent-skill.js +4 -2
- package/src/sqm/provider-command.js +1 -1
- package/src/sqm/provider-plugin-repair.js +105 -0
- package/src/sqm/provider-plugin-status.js +20 -8
package/README.md
CHANGED
|
@@ -60,6 +60,22 @@ Provider login is separate from model access; unverified model access remains
|
|
|
60
60
|
files scoped to the Web/account/organization, rotate each writer's four 64 KiB
|
|
61
61
|
segments, and keep lifetime failure counts after event rotation.
|
|
62
62
|
|
|
63
|
+
Each AI app card separates **SIMY connection: on / stopped** from CLI, sign-in
|
|
64
|
+
and plugin health. Disconnecting collapses those details and explains that SIMY
|
|
65
|
+
monitoring and automatic continuation are stopped. Details can still be opened;
|
|
66
|
+
reconnecting restores the item statuses. Plugin inspection failure never changes
|
|
67
|
+
the connection setting or hides its disconnect action.
|
|
68
|
+
|
|
69
|
+
Connection settings show **Repair automatically** beside a failed, missing or
|
|
70
|
+
disabled SIMY plugin. Clicking it rechecks only that provider with a longer,
|
|
71
|
+
bounded timeout; a missing or disabled managed plugin is installed or enabled,
|
|
72
|
+
then its actual inventory is checked again. Success is shown only after that
|
|
73
|
+
inventory confirms the plugin is enabled. Repair progress and failure reasons
|
|
74
|
+
are displayed in the card; safe reason codes and elapsed times are saved in
|
|
75
|
+
`$SIMY_HOME/logs/desktop-host.log`. Repair preserves SIMY connection consent,
|
|
76
|
+
provider sign-in and native hook permissions. Project/managed settings and
|
|
77
|
+
conflicting plugin sources require review instead of being overwritten.
|
|
78
|
+
|
|
63
79
|
The desktop host continues sampling connection and access while optional
|
|
64
80
|
conversation metadata is pending. It retains the last completed conversation
|
|
65
81
|
metadata only within the same verified scope, clears it after access or account
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@awak-app/simy-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.22",
|
|
4
4
|
"description": "Local SIMY executor for non-coding Local Tasks and Agentic Loop runs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"check:session-supervision-syntax": "node --check ./src/session-supervision/session-list.js && node --check ./src/console/sessions.js && node --check ./src/session-supervision/remote-hosts.js && node --check ./src/session-supervision/remote-entry.js && node --check ./src/session-supervision/index.js && node --check ./src/session-supervision/discovery-window.js && node --check ./src/session-supervision/codex-app-server.js && node --check ./src/session-supervision/claude-sessions.js && node --check ./src/session-supervision/original-session-target.js && node --check ./src/session-supervision/action-association.js && node --check ./src/session-supervision/action-association-host.js && node --check ./src/session-supervision/action-association-api.js && node --check ./src/session-supervision/association-store.js && node --check ./src/session-supervision/action-deadline.js && node --check ./src/session-supervision/action-deadline-store.js && node --check ./src/codex-plugins/simy/hooks/action-association.mjs && node --check ./src/session-supervision/purpose-instruction.js && node --check ./src/session-supervision/purpose-recovery.js && node --check ./src/session-supervision/purpose-candidate.js && node --check ./src/codex-plugins/simy/hooks/scene-reasoning.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-cycle.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-rule-review.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-search.mjs && node --check ./src/codex-plugins/simy/hooks/purpose-evidence.mjs && node --check ./src/session-supervision/purpose-observer.js && node --check ./src/session-supervision/purpose-verification.js && node --check ./src/codex-plugins/simy/hooks/purpose-workspace.mjs && node --check ./scripts/purpose-search-live-e2e.js",
|
|
43
43
|
"check:auto-update": "node --check ./src/auto-update.js && node --check ./scripts/auto-update-e2e.js && npm run check:sqm",
|
|
44
44
|
"check:workflow-observation-sender": "node --check ./src/workflow-observation-sender.js",
|
|
45
|
-
"check:sqm": "node --check ./src/sqm/advisory-guidance.js && node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/history-store.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
|
|
45
|
+
"check:sqm": "node --check ./src/sqm/provider-plugin-repair.js && node --check ./src/sqm/provider-plugin-status.js && node --check ./src/sqm/advisory-guidance.js && node --check ./src/sqm/canonical.js && node --check ./src/sqm/validation.js && node --check ./src/sqm/reliability.js && node --check ./src/sqm/repository.js && node --check ./src/sqm/checkers.js && node --check ./src/sqm/bundle-store.js && node --check ./src/sqm/proof.js && node --check ./src/sqm/evidence-client.js && node --check ./src/sqm/session.js && node --check ./src/sqm/index.js && node --check ./src/sqm/command.js && node --check ./src/sqm/agent-skill.js && node --check ./src/sqm/workflow-onboarding.js && node --check ./src/sqm/workflow-update.js && node --check ./src/sqm/history-store.js && node --check ./src/sqm/review-worker.js && node --check ./scripts/sqm-complete-loop-e2e.js && node --check ./scripts/sqm-local-review-e2e.js",
|
|
46
46
|
"check:real-provider-syntax": "node --check ./scripts/real-codex-probe.js && node --check ./scripts/real-claude-probe.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/claude-parity-runtime-e2e.js && node --check ./scripts/claude-plugin-runtime-e2e.js && node --check ./scripts/codex-plugin-runtime-e2e.js",
|
|
47
47
|
"check:syntax": "node --check ./src/purpose-local-proof.js && node --check ./src/device-signing-identity.js && node --check ./src/index.js && node --check ./src/install-update.js && node --check ./src/agent.js && node --check ./src/connection-discovery.js && node --check ./src/connection-diagnostics.js && node --check ./src/launch-authorization.js && node --check ./src/codex-plugins/simy/hooks/connection-reminder.mjs && node --check ./src/codex-plugins/simy/hooks/dev-work.mjs && node --check ./src/dev-work-events.js && node --check ./src/browser.js && node --check ./src/web-api.js && node --check ./src/durable-local-task-worker.js && node --check ./src/durable-local-task-steps-contract.js && node --check ./src/bounded-local-task-subtasks-contract.js && node --check ./src/bounded-local-task-subtask-pool.js && node --check ./src/local-task-artifact-contract.js && node --check ./src/execution-capability-contract.js && node --check ./src/shutdown.js && node --check ./src/run-registry.js && node --check ./src/direct-executor.js && node --check ./src/execution-guardrail.js && node --check ./src/backend-executable.js && node --check ./src/login-shell-path.js && node --check ./src/hook-runtime.js && node --check ./src/provider-stream.js && node --check ./src/console/index.js && node --check ./src/console/app.js && node --check ./src/console/commands.js && node --check ./src/local-attachments.js && node --check ./src/repository-inventory.js && node --check ./src/session-store.js && node --check ./src/web-origin.js && node --check ./src/workspace-context.js && node --check ./src/orchestrator/index.js && node --check ./src/orchestrator/shared.js && node --check ./src/orchestrator/recovery.js && node --check ./src/orchestrator/risk.js && node --check ./src/orchestrator/contract.js && node --check ./src/orchestrator/completion-contract.js && node --check ./src/orchestrator/pr-preparation.js && node --check ./src/orchestrator/minimum-completion.js && node --check ./src/orchestrator/delivery/scripts/check-minimum-completion.mjs && node --check ./src/orchestrator/instruction.js && node --check ./src/orchestrator/problem-solving.js && node --check ./src/orchestrator/budget.js && node --check ./src/orchestrator/execution-io.js && node --check ./src/orchestrator/presentation.js && node --check ./src/orchestrator/result.js && node --check ./src/orchestrator/retry.js && node --check ./src/orchestrator/evidence.js && node --check ./src/orchestrator/independent-audit.js && node --check ./src/orchestrator/audit.js && node --check ./src/orchestrator/loop.js && node --check ./src/runner.js && node --check ./src/coding-run-store.js && node --check ./scripts/fixtures/completion-contract-fixture.js && node --check ./scripts/fixtures/fake-coding-backend.js && node --check ./scripts/e2e-terminal-evidence.js && node --check ./scripts/audit-gate-e2e.js && node --check ./scripts/completion-contract-e2e.js && node --check ./scripts/retry-circuit-e2e.js && node --check ./scripts/follow-up-charter-e2e.js && node --check ./scripts/task-routing-e2e.js && node --check ./scripts/executor-version-preflight-e2e.js && node --check ./scripts/blocked-recovery-e2e.js && node --check ./scripts/process-localization-e2e.js && node --check ./scripts/hypothesis-retries-e2e.js && node --check ./scripts/budget-enforcement-e2e.js && node --check ./scripts/local-task-default-workspace-e2e.js && node --check ./scripts/local-task-idempotency-e2e.js && node --check ./scripts/console-e2e-fixture.js && node --check ./scripts/console-e2e.js && node --check ./scripts/console-recovery-e2e-fixture.js && node --check ./scripts/console-recovery-e2e.js && node --check ./scripts/real-provider-console-fixture.js && node --check ./scripts/real-provider-console-e2e.js && node --check ./scripts/fullstack-cli-smoke.js && node --check ./scripts/check-package-contents.js && node --check ./src/codex-plugins/simy/hooks/delivery-loop.mjs && node --check ./src/delivery-hooks/command.js && node --check ./src/delivery-hooks/desktop.js && node --check ./src/delivery-hooks/supervisor.js && node --check ./src/delivery-hooks/recovery.js && node --check ./src/delivery-hooks/supervisor-owner.js && node --check ./src/delivery-hooks/supervisor-watchdog.js && node --check ./src/delivery-hooks/supervisor-service.js && node --check ./scripts/supervisor-watchdog-live-e2e.js && node --check ./src/codex-plugins/simy/hooks/recovery-incident.mjs && node --check ./src/session-supervision/automatic-continuation.js && node --check ./src/session-supervision/stop-notifications.js",
|
|
48
48
|
"check:package": "node ./scripts/check-package-contents.js",
|
package/src/cli-contract.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./durable-local-task-steps-contract.js";
|
|
15
15
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
16
16
|
|
|
17
|
-
export const CLI_VERSION = "0.7.
|
|
17
|
+
export const CLI_VERSION = "0.7.22";
|
|
18
18
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
19
19
|
|
|
20
20
|
export function narrativeImageCapabilityMetadata() {
|
package/src/desktop-host.js
CHANGED
|
@@ -15,6 +15,7 @@ import { randomUUID } from "node:crypto";
|
|
|
15
15
|
import { readCodexStatistics, unavailableStatistics } from './session-supervision/statistics.js';
|
|
16
16
|
import { readClaudeStatistics } from './session-supervision/claude-sessions.js';
|
|
17
17
|
import { inspectProviderPlugins, unavailablePluginStatus } from './sqm/provider-plugin-status.js';
|
|
18
|
+
import { repairProviderPlugin } from './sqm/provider-plugin-repair.js';
|
|
18
19
|
import { openCodexAppServer } from './session-supervision/codex-app-server.js';
|
|
19
20
|
import { conversationSqmChecks } from './session-supervision/sqm-presentation.js';
|
|
20
21
|
import { stopOwnedDesktopHost } from "./desktop-force-quit.js";
|
|
@@ -503,6 +504,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
503
504
|
const changeProviderLink = async (provider, connected) => {
|
|
504
505
|
if (!PROVIDER_LINKS.includes(provider)) throw new Error("Unsupported SIMY provider connection.");
|
|
505
506
|
if (providerLinkBusy.has(provider)) throw new Error("SIMY connection is already changing.");
|
|
507
|
+
providerRepairControllers.get(provider)?.abort();
|
|
506
508
|
providerLinkBusy.add(provider);
|
|
507
509
|
providerLinkFeedback[provider] = { state: connected ? "connecting" : "disconnecting" };
|
|
508
510
|
let phase = "read_settings";
|
|
@@ -575,6 +577,11 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
575
577
|
const providerAuthBusy = new Set();
|
|
576
578
|
let pluginInstallationActive = false, pluginSetupEpoch = 0;
|
|
577
579
|
let providerPlugins = { codex: { state: 'checking' }, claude: { state: 'checking' } };
|
|
580
|
+
const providerRepairs = {}, providerRepairControllers = new Map(), providerRepairJobs = new Set();
|
|
581
|
+
const providerPluginEpochs = { codex: 0, claude: 0 };
|
|
582
|
+
const pluginDiagnostic = ({ provider, reason, elapsedMs }) => {
|
|
583
|
+
diagnostics.write(`provider plugin inspection provider=${provider} reason=${reason} elapsedMs=${elapsedMs}`);
|
|
584
|
+
};
|
|
578
585
|
let pluginInspection = null, lastPluginInspectionAt = -Infinity;
|
|
579
586
|
let pluginInspectionAbort = new AbortController();
|
|
580
587
|
const refreshProviderPlugins = () => {
|
|
@@ -582,9 +589,17 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
582
589
|
if (pluginInspection) return pluginInspection;
|
|
583
590
|
if (pluginInspectionAbort.signal.aborted) pluginInspectionAbort = new AbortController();
|
|
584
591
|
lastPluginInspectionAt = Date.now();
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
592
|
+
const epochs = { ...providerPluginEpochs };
|
|
593
|
+
const apply = value => {
|
|
594
|
+
for (const provider of PROVIDER_LINKS) {
|
|
595
|
+
if (!providerRepairControllers.has(provider) && epochs[provider] === providerPluginEpochs[provider]) {
|
|
596
|
+
providerPlugins[provider] = value[provider] || unavailablePluginStatus();
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
pluginInspection = Promise.resolve().then(() => (dependencies.inspectProviderPlugins || inspectProviderPlugins)({ signal: pluginInspectionAbort.signal, details: true, onDiagnostic: pluginDiagnostic }))
|
|
601
|
+
.then(apply)
|
|
602
|
+
.catch(() => apply({ codex: unavailablePluginStatus(), claude: unavailablePluginStatus() }))
|
|
588
603
|
.finally(() => { pluginInspection = null; if (!closed && !relay) void publish().catch(() => {}); });
|
|
589
604
|
return pluginInspection;
|
|
590
605
|
};
|
|
@@ -608,6 +623,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
608
623
|
}]));
|
|
609
624
|
status.pluginSetup = { ...pluginSetup };
|
|
610
625
|
status.providerPlugins = structuredClone(providerPlugins);
|
|
626
|
+
status.providerRepairs = structuredClone(providerRepairs);
|
|
611
627
|
for (const provider of ["codex", "claude"]) {
|
|
612
628
|
if (providerAuth[provider]?.state === "awaiting_sign_in" && !providerAuthBusy.has(provider) && status.providers?.[provider]?.state === "ready") delete providerAuth[provider];
|
|
613
629
|
}
|
|
@@ -804,6 +820,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
804
820
|
clearInterval(timer);
|
|
805
821
|
teamThreads?.close();
|
|
806
822
|
pluginInspectionAbort.abort();
|
|
823
|
+
for (const controller of providerRepairControllers.values()) controller.abort();
|
|
807
824
|
activeStatisticsRead?.controller.abort();
|
|
808
825
|
discoveryStopped = true;
|
|
809
826
|
encodedStatus = null; lastStatus = null;
|
|
@@ -812,6 +829,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
812
829
|
// reader must never postpone cancellation of an owned Provider process.
|
|
813
830
|
const results = await Promise.allSettled([stopDiscovery({ drain: true }), publishing, connectionPublishing,
|
|
814
831
|
pluginInspection,
|
|
832
|
+
...providerRepairJobs,
|
|
815
833
|
remoteHosts?.close(), actionAssociations.close(), automaticContinuation?.close(), analysisManager?.close(),
|
|
816
834
|
managedSupervisor.close(), recordReader.close(), stopWorkflows(),
|
|
817
835
|
agent ? createAgentShutdown(agent)() : undefined]);
|
|
@@ -1163,7 +1181,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1163
1181
|
}
|
|
1164
1182
|
|
|
1165
1183
|
let commandProviderLink;
|
|
1166
|
-
if (command.provider && !["refresh", "provider_connect", "provider_disconnect", "delivery_stop", "delivery_pause", "delivery_reject", "session_untrack", "session_unmonitor"].includes(action) &&
|
|
1184
|
+
if (command.provider && !["provider_repair", "refresh", "provider_connect", "provider_disconnect", "delivery_stop", "delivery_pause", "delivery_reject", "session_untrack", "session_unmonitor"].includes(action) &&
|
|
1167
1185
|
!(action === "session_set_auto_continue" && command.enabled === false)) {
|
|
1168
1186
|
commandProviderLink = await requireProviderLink({ root: deliveryRoot, provider: command.provider });
|
|
1169
1187
|
}
|
|
@@ -1188,7 +1206,48 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1188
1206
|
await remoteHosts.save({ alias: command.alias, label: command.label }, action === 'remote_host_remove');
|
|
1189
1207
|
emit({ type: 'result', id: command.id, ok: true }); await publish(); return;
|
|
1190
1208
|
}
|
|
1191
|
-
if (action ===
|
|
1209
|
+
if (action === 'provider_repair') {
|
|
1210
|
+
const provider = command.provider;
|
|
1211
|
+
if (!PROVIDER_LINKS.includes(provider)) throw new Error('Unsupported provider.');
|
|
1212
|
+
if (providerRepairControllers.has(provider) || providerLinkBusy.has(provider)) throw new Error('Provider repair is already running.');
|
|
1213
|
+
const controller = new AbortController();
|
|
1214
|
+
providerRepairControllers.set(provider, controller);
|
|
1215
|
+
providerPluginEpochs[provider]++;
|
|
1216
|
+
providerRepairs[provider] = { state: 'repairing' };
|
|
1217
|
+
const beganAt = Date.now();
|
|
1218
|
+
let repairJob;
|
|
1219
|
+
try {
|
|
1220
|
+
await publish();
|
|
1221
|
+
repairJob = Promise.resolve().then(() => (dependencies.repairProviderPlugin || repairProviderPlugin)({
|
|
1222
|
+
provider, signal: controller.signal, onDiagnostic: pluginDiagnostic,
|
|
1223
|
+
authorize: async () => {
|
|
1224
|
+
if (closed || relay || controller.signal.aborted) throw Object.assign(new Error('Repair cancelled.'), { reason: 'cancelled' });
|
|
1225
|
+
await authorizeWork(commandScope);
|
|
1226
|
+
},
|
|
1227
|
+
}));
|
|
1228
|
+
providerRepairJobs.add(repairJob);
|
|
1229
|
+
const result = await repairJob;
|
|
1230
|
+
if (closed || controller.signal.aborted) throw Object.assign(new Error('Repair cancelled.'), { reason: 'cancelled' });
|
|
1231
|
+
await authorizeWork(commandScope);
|
|
1232
|
+
if (result?.plugin?.state !== 'enabled') throw Object.assign(new Error('Repair was not verified.'), { reason: 'verification_failed' });
|
|
1233
|
+
providerPlugins[provider] = result.plugin;
|
|
1234
|
+
providerRepairs[provider] = { state: 'repaired' };
|
|
1235
|
+
diagnostics.write(`provider plugin repair provider=${provider} result=repaired elapsedMs=${Date.now() - beganAt}`);
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
const reasons = ['timeout', 'cli_missing', 'invalid_inventory', 'command_failed', 'verification_failed',
|
|
1238
|
+
'scope_requires_attention', 'source_conflict', 'busy', 'permission_denied', 'cancelled'];
|
|
1239
|
+
const reason = reasons.includes(error?.reason) ? error.reason : 'command_failed';
|
|
1240
|
+
providerRepairs[provider] = { state: 'failed', reason };
|
|
1241
|
+
diagnostics.write(`provider plugin repair provider=${provider} result=failed reason=${reason} elapsedMs=${Date.now() - beganAt}`);
|
|
1242
|
+
throw error;
|
|
1243
|
+
} finally {
|
|
1244
|
+
providerRepairJobs.delete(repairJob);
|
|
1245
|
+
providerRepairControllers.delete(provider);
|
|
1246
|
+
await publish();
|
|
1247
|
+
}
|
|
1248
|
+
emit({ type: 'result', id: command.id, ok: true });
|
|
1249
|
+
return;
|
|
1250
|
+
} else if (action === "provider_connect" || action === "provider_disconnect") {
|
|
1192
1251
|
await changeProviderLink(command.provider, action === "provider_connect");
|
|
1193
1252
|
emit({ type: "result", id: command.id, ok: true });
|
|
1194
1253
|
return;
|
|
@@ -1598,7 +1657,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
1598
1657
|
void execute();
|
|
1599
1658
|
} else if (["login", "organization_select", "organization_cancel", "authorization_open"].includes(command.action)) {
|
|
1600
1659
|
authorizationQueue = authorizationQueue.then(execute).catch(() => {});
|
|
1601
|
-
} else if (/^(codex|claude)_login$/.test(command.action || "") || ["provider_connect", "provider_disconnect"].includes(command.action)) {
|
|
1660
|
+
} else if (/^(codex|claude)_login$/.test(command.action || "") || ["provider_repair", "provider_connect", "provider_disconnect"].includes(command.action)) {
|
|
1602
1661
|
// Opening an auth terminal must not wait for an unrelated provider turn.
|
|
1603
1662
|
// Per-provider busy guards prevent overlapping connection changes.
|
|
1604
1663
|
void execute();
|
package/src/sqm/agent-skill.js
CHANGED
|
@@ -100,14 +100,16 @@ export async function inspectSqmAgentSkill({ home = homedir() } = {}) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
export async function installSqmAgentSkill({ home = homedir(), platforms = Object.keys(TARGETS) } = {}) {
|
|
103
|
+
export async function installSqmAgentSkill({ home = homedir(), platforms = Object.keys(TARGETS), migrateLegacyGate = true } = {}) {
|
|
104
104
|
const requested = [...new Set(platforms)];
|
|
105
105
|
if (!requested.length || requested.some((platform) => !Object.hasOwn(TARGETS, platform))) {
|
|
106
106
|
throw new Error("platforms must contain codex and/or claude");
|
|
107
107
|
}
|
|
108
108
|
if (requested.includes("codex")) await installCodexPlugin({ home });
|
|
109
109
|
if (requested.includes("claude")) await installClaudePlugin({ home });
|
|
110
|
-
|
|
110
|
+
// Explicit single-provider repair must not migrate another provider's hooks.
|
|
111
|
+
// Ordinary setup and workflow updates retain the existing migration behavior.
|
|
112
|
+
if (migrateLegacyGate) await retireLegacySqmGate({ home });
|
|
111
113
|
for (const platform of requested) {
|
|
112
114
|
await atomicText(path.join(home, ...TARGETS[platform]), SKILL_CONTENT, 0o600);
|
|
113
115
|
}
|
|
@@ -16,7 +16,7 @@ export function runProviderCommand(command, args, { env, cwd, timeout = 10_000,
|
|
|
16
16
|
};
|
|
17
17
|
const timer = setTimeout(() => {
|
|
18
18
|
child.kill("SIGTERM");
|
|
19
|
-
finish(new Error("Provider plugin command timed out."));
|
|
19
|
+
finish(Object.assign(new Error("Provider plugin command timed out."), { code: 'provider_timeout' }));
|
|
20
20
|
}, timeout);
|
|
21
21
|
timer.unref?.();
|
|
22
22
|
const capture = (stream, append) => {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { mkdir, readFile, realpath, rm } from 'node:fs/promises';
|
|
4
|
+
import { resolveBackendExecutable } from '../backend-executable.js';
|
|
5
|
+
import { inspectProviderPlugins } from './provider-plugin-status.js';
|
|
6
|
+
import { runProviderCommand } from './provider-command.js';
|
|
7
|
+
import { inspectSqmAgentSkill, installSqmAgentSkill } from './agent-skill.js';
|
|
8
|
+
import { CLAUDE_MARKETPLACE, CLAUDE_PLUGIN_SELECTOR } from './claude-plugin.js';
|
|
9
|
+
|
|
10
|
+
const fail = reason => Object.assign(new Error('SIMY plugin repair could not be verified.'), { code: 'plugin_repair_failed', reason });
|
|
11
|
+
|
|
12
|
+
// Explicit user action only. Repair one provider, under the same lock as the
|
|
13
|
+
// background workflow updater. Never change SIMY consent, login or hook trust.
|
|
14
|
+
export async function repairProviderPlugin({ provider, home = homedir(),
|
|
15
|
+
codexHome = process.env.CODEX_HOME || join(home, '.codex'),
|
|
16
|
+
claudeHome = process.env.CLAUDE_CONFIG_DIR || join(home, '.claude'),
|
|
17
|
+
signal, authorize = async () => {}, onDiagnostic = () => {},
|
|
18
|
+
inspect = inspectProviderPlugins, inspectSources = inspectSqmAgentSkill,
|
|
19
|
+
prepare = installSqmAgentSkill, resolveExecutable = resolveBackendExecutable,
|
|
20
|
+
run = runProviderCommand,
|
|
21
|
+
} = {}) {
|
|
22
|
+
if (!['codex', 'claude'].includes(provider)) throw fail('unsupported_provider');
|
|
23
|
+
const startedAt = Date.now();
|
|
24
|
+
const check = async () => {
|
|
25
|
+
if (signal?.aborted) throw fail('cancelled');
|
|
26
|
+
await authorize();
|
|
27
|
+
if (signal?.aborted) throw fail('cancelled');
|
|
28
|
+
};
|
|
29
|
+
const options = { home, codexHome, claudeHome, providers: [provider], details: true,
|
|
30
|
+
timeoutMs: 20_000, signal, onDiagnostic, resolveExecutable, run };
|
|
31
|
+
const inventory = async () => (await inspect(options))[provider];
|
|
32
|
+
const command = async args => {
|
|
33
|
+
await check();
|
|
34
|
+
const executable = await resolveExecutable(provider, { signal, timeoutMs: 5_000 });
|
|
35
|
+
if (!executable) throw fail('cli_missing');
|
|
36
|
+
return run(executable, args, { cwd: home, signal, timeout: 20_000, maxBuffer: 1024 * 1024,
|
|
37
|
+
env: { ...process.env, HOME: home, ...(provider === 'codex' ? { CODEX_HOME: codexHome } : { CLAUDE_CONFIG_DIR: claudeHome }) } });
|
|
38
|
+
};
|
|
39
|
+
let locked = false;
|
|
40
|
+
const lock = join(home, '.simy', 'integrations', 'workflow-update.lock');
|
|
41
|
+
try {
|
|
42
|
+
await check();
|
|
43
|
+
let current = await inventory();
|
|
44
|
+
if (current?.state === 'enabled') return { state: 'repaired', plugin: current };
|
|
45
|
+
if (!['missing', 'disabled'].includes(current?.state)) throw fail(current?.reason || 'verification_failed');
|
|
46
|
+
if (provider === 'claude' && current.scope && current.scope !== 'user') throw fail('scope_requires_attention');
|
|
47
|
+
await mkdir(join(home, '.simy', 'integrations'), { recursive: true });
|
|
48
|
+
try { await mkdir(lock); locked = true; }
|
|
49
|
+
catch (error) { if (error.code === 'EEXIST') throw fail('busy'); throw error; }
|
|
50
|
+
await check();
|
|
51
|
+
// Reinspect after obtaining the lock; another updater may have finished.
|
|
52
|
+
current = await inventory();
|
|
53
|
+
if (current?.state === 'enabled') return { state: 'repaired', plugin: current };
|
|
54
|
+
if (!['missing', 'disabled'].includes(current?.state)) throw fail(current?.reason || 'verification_failed');
|
|
55
|
+
if (provider === 'claude' && current.scope && current.scope !== 'user') throw fail('scope_requires_attention');
|
|
56
|
+
let sources = await inspectSources({ home });
|
|
57
|
+
if (provider === 'codex' && current.state === 'disabled') {
|
|
58
|
+
const payload = JSON.parse((await command(['plugin', 'list', '--json'])).stdout);
|
|
59
|
+
const entry = payload.installed?.find(item => item.pluginId === current.selector);
|
|
60
|
+
if (entry?.source?.source !== 'local' || !entry.source.path ||
|
|
61
|
+
await realpath(entry.source.path) !== await realpath(sources.codex_plugin.path)) throw fail('source_conflict');
|
|
62
|
+
const marketplace = JSON.parse(await readFile(sources.codex_plugin.marketplace_path, 'utf8'));
|
|
63
|
+
if (current.selector !== `simy@${marketplace.name}`) throw fail('source_conflict');
|
|
64
|
+
}
|
|
65
|
+
if (provider === 'claude') {
|
|
66
|
+
const marketplaces = JSON.parse((await command(['plugin', 'marketplace', 'list', '--json'])).stdout);
|
|
67
|
+
if (!Array.isArray(marketplaces)) throw fail('invalid_inventory');
|
|
68
|
+
const existing = marketplaces.find(item => item.name === CLAUDE_MARKETPLACE);
|
|
69
|
+
if (existing && (existing.source !== 'directory' || !existing.path ||
|
|
70
|
+
await realpath(existing.path) !== await realpath(sources.claude_plugin.marketplace_root))) throw fail('source_conflict');
|
|
71
|
+
}
|
|
72
|
+
await check();
|
|
73
|
+
sources = await prepare({ home, platforms: [provider], migrateLegacyGate: false });
|
|
74
|
+
if (provider === 'codex') {
|
|
75
|
+
const marketplace = JSON.parse(await readFile(sources.codex_plugin.marketplace_path, 'utf8'));
|
|
76
|
+
if (!/^[A-Za-z0-9_-]+$/.test(marketplace.name || '')) throw fail('source_conflict');
|
|
77
|
+
const selector = `simy@${marketplace.name}`;
|
|
78
|
+
if (current.selector && current.selector !== selector) throw fail('source_conflict');
|
|
79
|
+
await command(['plugin', 'add', selector, '--json']);
|
|
80
|
+
} else {
|
|
81
|
+
// The managed marketplace installer preserves unrelated provider settings.
|
|
82
|
+
const marketplaces = JSON.parse((await command(['plugin', 'marketplace', 'list', '--json'])).stdout);
|
|
83
|
+
if (!Array.isArray(marketplaces)) throw fail('invalid_inventory');
|
|
84
|
+
const existing = marketplaces.find(item => item.name === CLAUDE_MARKETPLACE);
|
|
85
|
+
if (existing && (existing.source !== 'directory' || !existing.path ||
|
|
86
|
+
await realpath(existing.path) !== await realpath(sources.claude_plugin.marketplace_root))) throw fail('source_conflict');
|
|
87
|
+
if (!existing) {
|
|
88
|
+
await command(['plugin', 'marketplace', 'add', sources.claude_plugin.marketplace_root]);
|
|
89
|
+
}
|
|
90
|
+
await command(['plugin', current.state === 'missing' ? 'install' : 'enable', CLAUDE_PLUGIN_SELECTOR, '--scope', 'user']);
|
|
91
|
+
}
|
|
92
|
+
await check();
|
|
93
|
+
const verified = await inventory();
|
|
94
|
+
if (verified?.state !== 'enabled') throw fail(verified?.reason || 'verification_failed');
|
|
95
|
+
return { state: 'repaired', plugin: verified };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
const reason = error?.reason || (signal?.aborted ? 'cancelled' : error?.code === 'provider_timeout' ? 'timeout' :
|
|
98
|
+
error instanceof SyntaxError ? 'invalid_inventory' :
|
|
99
|
+
['EACCES', 'EPERM', 'EROFS'].includes(error?.code) ? 'permission_denied' : 'command_failed');
|
|
100
|
+
onDiagnostic({ provider, reason, elapsedMs: Date.now() - startedAt });
|
|
101
|
+
throw fail(reason);
|
|
102
|
+
} finally {
|
|
103
|
+
if (locked) await rm(lock, { recursive: true, force: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -14,13 +14,22 @@ export async function inspectProviderPlugins({ home = homedir(),
|
|
|
14
14
|
codexHome = process.env.CODEX_HOME || join(home, '.codex'),
|
|
15
15
|
claudeHome = process.env.CLAUDE_CONFIG_DIR || join(home, '.claude'),
|
|
16
16
|
resolveExecutable = resolveBackendExecutable, run = runProviderCommand,
|
|
17
|
-
timeoutMs = 5_000, signal,
|
|
17
|
+
timeoutMs = 5_000, signal, providers = ['codex', 'claude'], details = false,
|
|
18
|
+
onDiagnostic = () => {},
|
|
18
19
|
} = {}) {
|
|
20
|
+
if (providers.some(provider => !['codex', 'claude'].includes(provider))) throw new Error('Unsupported provider.');
|
|
19
21
|
const inspect = async provider => {
|
|
22
|
+
const startedAt = Date.now();
|
|
23
|
+
const unavailable = reason => {
|
|
24
|
+
const elapsedMs = Date.now() - startedAt;
|
|
25
|
+
onDiagnostic({ provider, reason, elapsedMs });
|
|
26
|
+
return { ...unavailablePluginStatus(), ...(details ? { reason, elapsedMs } : {}) };
|
|
27
|
+
};
|
|
20
28
|
try {
|
|
21
|
-
if (signal?.aborted) return
|
|
29
|
+
if (signal?.aborted) return unavailable('cancelled');
|
|
22
30
|
const executable = await resolveExecutable(provider, { signal, timeoutMs });
|
|
23
|
-
if (
|
|
31
|
+
if (signal?.aborted) return unavailable('cancelled');
|
|
32
|
+
if (!executable) return unavailable('cli_missing');
|
|
24
33
|
const { stdout } = await run(executable, ['plugin', 'list', '--json'], {
|
|
25
34
|
timeout: timeoutMs, maxBuffer: 1024 * 1024, signal, cwd: home,
|
|
26
35
|
env: { ...process.env, HOME: home,
|
|
@@ -28,7 +37,7 @@ export async function inspectProviderPlugins({ home = homedir(),
|
|
|
28
37
|
});
|
|
29
38
|
const payload = JSON.parse(stdout);
|
|
30
39
|
const entries = provider === 'codex' ? payload?.installed : payload;
|
|
31
|
-
if (!Array.isArray(entries)) return
|
|
40
|
+
if (!Array.isArray(entries)) return unavailable('invalid_inventory');
|
|
32
41
|
const matches = entries.filter(entry => provider === 'codex'
|
|
33
42
|
? typeof entry?.pluginId === 'string' && /^simy@[A-Za-z0-9_-]+$/.test(entry.pluginId)
|
|
34
43
|
: entry?.id === CLAUDE_PLUGIN_SELECTOR);
|
|
@@ -42,11 +51,14 @@ export async function inspectProviderPlugins({ home = homedir(),
|
|
|
42
51
|
const entry = matches[0];
|
|
43
52
|
if (!entry || provider === 'codex' && entry.installed === false) return { state: 'missing', installed: false, enabled: false };
|
|
44
53
|
if (typeof entry.enabled !== 'boolean' || provider === 'codex' && entry.installed !== true ||
|
|
45
|
-
provider === 'claude' && !scopes.includes(entry.scope)) return
|
|
54
|
+
provider === 'claude' && !scopes.includes(entry.scope)) return unavailable('invalid_inventory');
|
|
46
55
|
return { state: entry.enabled ? 'enabled' : 'disabled', installed: true, enabled: entry.enabled,
|
|
56
|
+
...(details ? { selector: provider === 'codex' ? entry.pluginId : entry.id } : {}),
|
|
47
57
|
...(provider === 'claude' ? { scope: entry.scope, enablementContext: 'home' } : {}) };
|
|
48
|
-
} catch
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return unavailable(signal?.aborted ? 'cancelled' : error?.code === 'provider_timeout' ? 'timeout' :
|
|
60
|
+
error instanceof SyntaxError ? 'invalid_inventory' : 'command_failed');
|
|
61
|
+
}
|
|
49
62
|
};
|
|
50
|
-
|
|
51
|
-
return { codex, claude };
|
|
63
|
+
return Object.fromEntries(await Promise.all(providers.map(async provider => [provider, await inspect(provider)])));
|
|
52
64
|
}
|