@awak-app/simy-cli 0.6.5 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -218,8 +218,13 @@ Codex or Claude Code conversation. Install the plugin, review and enable SIMY ho
|
|
|
218
218
|
with `/hooks`, then request `/simy:delivery-loop` for the task. Installing or updating
|
|
219
219
|
the plugin never approves hook trust. SIMY's Connection & settings panel links to
|
|
220
220
|
Codex settings and the Claude setup guide and shows observed hook activity and
|
|
221
|
-
blocked/recovery states.
|
|
222
|
-
|
|
221
|
+
blocked/recovery states.
|
|
222
|
+
The Connection tab also checks each provider's actual SIMY plugin inventory and
|
|
223
|
+
shows installed/enabled, installed/disabled, missing, or unavailable separately
|
|
224
|
+
from app sign-in. Claude enablement is checked from the home folder; project
|
|
225
|
+
settings can differ, and project-scoped installations are labeled explicitly.
|
|
226
|
+
The legacy `agentic-delivery-loop` skill remains retired; the Web/CLI executor is
|
|
227
|
+
unchanged. See [hook delivery behavior and limits](docs/delivery-hooks.md).
|
|
223
228
|
`--no-auto-update` or `SIMY_AUTO_UPDATE=0` disables these background updates too;
|
|
224
229
|
`--no-workflow-prompt` only suppresses the initial installation prompt.
|
|
225
230
|
|
package/package.json
CHANGED
package/src/cli-contract.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "./durable-local-task-steps-contract.js";
|
|
14
14
|
import { buildExecutionCapabilityContract } from "./execution-capability-contract.js";
|
|
15
15
|
|
|
16
|
-
export const CLI_VERSION = "0.6.
|
|
16
|
+
export const CLI_VERSION = "0.6.7";
|
|
17
17
|
export const CLI_API_CONTRACT_VERSION = 10;
|
|
18
18
|
|
|
19
19
|
export function withCliContract(
|
package/src/desktop-host.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readCodexStatistics, unavailableStatistics } from './session-supervision/statistics.js';
|
|
2
2
|
import { readClaudeStatistics } from './session-supervision/claude-sessions.js';
|
|
3
|
+
import { inspectProviderPlugins, unavailablePluginStatus } from './sqm/provider-plugin-status.js';
|
|
3
4
|
import { openCodexAppServer } from './session-supervision/codex-app-server.js';
|
|
4
5
|
import { conversationSqmChecks } from './session-supervision/sqm-presentation.js';
|
|
5
6
|
import { stopOwnedDesktopHost } from "./desktop-force-quit.js";
|
|
@@ -196,12 +197,27 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
196
197
|
onChange: () => { if (!closed && !relay) refreshSessions(true); },
|
|
197
198
|
});
|
|
198
199
|
let pluginSetup = { state: "unknown" };
|
|
200
|
+
let providerPlugins = { codex: { state: 'checking' }, claude: { state: 'checking' } };
|
|
201
|
+
let pluginInspection = null, lastPluginInspectionAt = -Infinity;
|
|
202
|
+
let pluginInspectionAbort = new AbortController();
|
|
203
|
+
const refreshProviderPlugins = () => {
|
|
204
|
+
if (closed || relay) return Promise.resolve();
|
|
205
|
+
if (pluginInspection) return pluginInspection;
|
|
206
|
+
if (pluginInspectionAbort.signal.aborted) pluginInspectionAbort = new AbortController();
|
|
207
|
+
lastPluginInspectionAt = Date.now();
|
|
208
|
+
pluginInspection = Promise.resolve().then(() => (dependencies.inspectProviderPlugins || inspectProviderPlugins)({ signal: pluginInspectionAbort.signal }))
|
|
209
|
+
.then(value => { providerPlugins = value; })
|
|
210
|
+
.catch(() => { providerPlugins = { codex: unavailablePluginStatus(), claude: unavailablePluginStatus() }; })
|
|
211
|
+
.finally(() => { pluginInspection = null; if (!closed && !relay) void publish().catch(() => {}); });
|
|
212
|
+
return pluginInspection;
|
|
213
|
+
};
|
|
199
214
|
const inspectPlugins = dependencies.inspectPlugins || inspectSqmAgentSkill;
|
|
200
215
|
const refreshPlugins = async () => {
|
|
201
216
|
try {
|
|
202
217
|
const status = await inspectPlugins();
|
|
203
218
|
pluginSetup = { state: status.installed ? "ready" : "missing", refreshPending: status.installed && pluginSetup.refreshPending === true };
|
|
204
219
|
} catch { pluginSetup = { state: "error" }; }
|
|
220
|
+
void refreshProviderPlugins();
|
|
205
221
|
};
|
|
206
222
|
const packageInfo = dependencies.packageInfo || await readCliPackageInfo();
|
|
207
223
|
diagnostics.write(`desktop starting version=${packageInfo.version} pid=${process.pid} role=${candidate ? "update-candidate" : "supervisor"}`);
|
|
@@ -224,6 +240,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
224
240
|
}
|
|
225
241
|
return scheduledPublish.promise;
|
|
226
242
|
};
|
|
243
|
+
let lastDeadlineFailureAt = -Infinity;
|
|
227
244
|
const projectStatus = () => {
|
|
228
245
|
if (closed || relay || !agent) return Promise.resolve();
|
|
229
246
|
if (publishing) { publishAgain = true; return publishing; }
|
|
@@ -233,6 +250,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
233
250
|
status.access = currentAccess;
|
|
234
251
|
const requestedScope = accessScope(currentAccess);
|
|
235
252
|
status.pluginSetup = { ...pluginSetup };
|
|
253
|
+
status.providerPlugins = structuredClone(providerPlugins);
|
|
236
254
|
if (!desktopAccessReady(currentAccess)) {
|
|
237
255
|
for (const item of sessionStatus.sessions) automaticContinuation?.revoke(item.provider, item.session);
|
|
238
256
|
if (currentAccess.state !== 'checking') await analysisManager.update([]);
|
|
@@ -258,9 +276,18 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
258
276
|
for (const session of sessions) {
|
|
259
277
|
session.statistics = statisticsCache.get(`${session.provider}:${session.session}`) || unavailableStatistics();
|
|
260
278
|
session.sqmActivity = conversationSqmChecks(session, [...(status.sqmChecks?.items || []), ...(status.sqmTasks?.items || [])]);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
session.actionDeadline = { status: 'unavailable', online: false, reminder: null };
|
|
280
|
+
try {
|
|
281
|
+
const context = await actionAssociations.deadlineContext?.(session, projection);
|
|
282
|
+
if (context) session.actionDeadline = await deadlines.reconcile(context, { displayOnly: true });
|
|
283
|
+
} catch {
|
|
284
|
+
// Optional per-conversation metadata must not stop catalog/connection
|
|
285
|
+
// heartbeats. Keep the fallback and bound diagnostics across refreshes.
|
|
286
|
+
if (Date.now() - lastDeadlineFailureAt >= 30_000) {
|
|
287
|
+
lastDeadlineFailureAt = Date.now();
|
|
288
|
+
diagnostics.write('conversation deadline unavailable; desktop status retained');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
264
291
|
const run = runsBySession.get(`${session.provider}:${session.session}`);
|
|
265
292
|
if (run) run.actionDeadline = session.actionDeadline;
|
|
266
293
|
}
|
|
@@ -274,12 +301,14 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
274
301
|
};
|
|
275
302
|
const stopAgent = async () => {
|
|
276
303
|
clearInterval(timer);
|
|
304
|
+
pluginInspectionAbort.abort();
|
|
277
305
|
discoveryStopped = true;
|
|
278
306
|
encodedStatus = null; lastStatus = null;
|
|
279
307
|
if (scheduledPublish) { clearTimeout(scheduledPublish.timer); scheduledPublish.resolve(); scheduledPublish = null; }
|
|
280
308
|
// Start every cancellation before draining discovery/network work. A hung
|
|
281
309
|
// reader must never postpone cancellation of an owned Provider process.
|
|
282
310
|
const results = await Promise.allSettled([stopDiscovery({ drain: true }), publishing,
|
|
311
|
+
pluginInspection,
|
|
283
312
|
actionAssociations.close(), automaticContinuation?.close(), analysisManager?.close(),
|
|
284
313
|
managedSupervisor.close(), recordReader.close(), stopWorkflows(),
|
|
285
314
|
agent ? createAgentShutdown(agent)() : undefined]);
|
|
@@ -406,6 +435,7 @@ export async function startDesktopHost({ input = process.stdin, output = process
|
|
|
406
435
|
tick(actionAssociations);
|
|
407
436
|
tick(managedSupervisor);
|
|
408
437
|
timer = setInterval(() => {
|
|
438
|
+
if (Date.now() - lastPluginInspectionAt >= 30_000) void refreshProviderPlugins();
|
|
409
439
|
if (desktopAccessReady(currentAccess)) {
|
|
410
440
|
tick(actionAssociations);
|
|
411
441
|
tick(managedSupervisor);
|
|
@@ -75,6 +75,11 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
75
75
|
const local = latest?.run?.managed?.actionAssociation;
|
|
76
76
|
if (latest?.run?.managed?.taskId !== record.run.managed.taskId) return latest;
|
|
77
77
|
latest.run.managed.actionAssociation = { ...local, authRevision: expected };
|
|
78
|
+
// Successful empty responses restore availability without erasing matches
|
|
79
|
+
// or pending work that still belongs to this authenticated scope.
|
|
80
|
+
if (local?.status === 'unavailable' && !local.binding && !local.job && !local.outbox) {
|
|
81
|
+
latest.run.managed.actionAssociation.status = 'waiting_boundary';
|
|
82
|
+
}
|
|
78
83
|
if (!local?.job) delete latest.run.managed.actionAssociation.invalidated;
|
|
79
84
|
return latest;
|
|
80
85
|
});
|
|
@@ -218,6 +223,11 @@ export function createActionAssociationHost({ root, sessionRoot, webOrigin, api
|
|
|
218
223
|
if (record?.cwd !== candidate.cwd) return null;
|
|
219
224
|
const local = record?.run?.managed?.actionAssociation || record?.actionAssociation;
|
|
220
225
|
if (!expected || projection && auth !== expected || local?.authRevision !== expected || !(candidate.actionAssociation?.canCorrect || candidate.actionAssociation?.canReadBinding)) return null;
|
|
226
|
+
// Correction can be available before the first scoped binding exists.
|
|
227
|
+
// Such a conversation cannot yet address a durable deadline record.
|
|
228
|
+
if (typeof local.namespace !== 'string' || !local.namespace.trim() ||
|
|
229
|
+
typeof candidate.taskId !== 'string' || !candidate.taskId.trim() ||
|
|
230
|
+
(record.run?.managed?.taskId || record.associationTaskId) !== candidate.taskId) return null;
|
|
221
231
|
const binding = candidate.actionAssociation.binding;
|
|
222
232
|
const fingerprint = value => JSON.stringify([value?.revision, value?.action?.id, value?.action?.revision, value?.action?.dueAt, value?.action?.timeZone, value?.action?.status]);
|
|
223
233
|
return { namespace: local.namespace, provider: candidate.provider, session: candidate.session, taskId: candidate.taskId, binding,
|
|
@@ -2,9 +2,9 @@ import spawn from "cross-spawn";
|
|
|
2
2
|
|
|
3
3
|
// cross-spawn resolves npm .cmd shims on Windows without interpolating command
|
|
4
4
|
// text. Keep updater commands bounded and preserve execFile-like test injection.
|
|
5
|
-
export function runProviderCommand(command, args, { env, timeout = 10_000, maxBuffer = 1024 * 1024 } = {}) {
|
|
5
|
+
export function runProviderCommand(command, args, { env, cwd, timeout = 10_000, maxBuffer = 1024 * 1024, signal } = {}) {
|
|
6
6
|
return new Promise((resolve, reject) => {
|
|
7
|
-
const child = spawn(command, args, { env, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
7
|
+
const child = spawn(command, args, { env, cwd, signal, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] });
|
|
8
8
|
let stdout = "";
|
|
9
9
|
let stderr = "";
|
|
10
10
|
let settled = false;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { resolveBackendExecutable } from '../backend-executable.js';
|
|
4
|
+
import { runProviderCommand } from './provider-command.js';
|
|
5
|
+
import { CLAUDE_PLUGIN_SELECTOR } from './claude-plugin.js';
|
|
6
|
+
|
|
7
|
+
export const unavailablePluginStatus = () => ({ state: 'unknown', installed: null, enabled: null });
|
|
8
|
+
|
|
9
|
+
// Provider inventories describe the actual installation and enablement. Managed
|
|
10
|
+
// source files and previously observed hooks cannot establish either state.
|
|
11
|
+
// Inspect from home so a launch directory cannot silently change Claude's merged
|
|
12
|
+
// enablement; expose that context because individual project overrides can differ.
|
|
13
|
+
export async function inspectProviderPlugins({ home = homedir(),
|
|
14
|
+
codexHome = process.env.CODEX_HOME || join(home, '.codex'),
|
|
15
|
+
claudeHome = process.env.CLAUDE_CONFIG_DIR || join(home, '.claude'),
|
|
16
|
+
resolveExecutable = resolveBackendExecutable, run = runProviderCommand,
|
|
17
|
+
timeoutMs = 5_000, signal,
|
|
18
|
+
} = {}) {
|
|
19
|
+
const inspect = async provider => {
|
|
20
|
+
try {
|
|
21
|
+
if (signal?.aborted) return unavailablePluginStatus();
|
|
22
|
+
const executable = await resolveExecutable(provider, { signal, timeoutMs });
|
|
23
|
+
if (!executable || signal?.aborted) return unavailablePluginStatus();
|
|
24
|
+
const { stdout } = await run(executable, ['plugin', 'list', '--json'], {
|
|
25
|
+
timeout: timeoutMs, maxBuffer: 1024 * 1024, signal, cwd: home,
|
|
26
|
+
env: { ...process.env, HOME: home,
|
|
27
|
+
...(provider === 'codex' ? { CODEX_HOME: codexHome } : { CLAUDE_CONFIG_DIR: claudeHome }) },
|
|
28
|
+
});
|
|
29
|
+
const payload = JSON.parse(stdout);
|
|
30
|
+
const entries = provider === 'codex' ? payload?.installed : payload;
|
|
31
|
+
if (!Array.isArray(entries)) return unavailablePluginStatus();
|
|
32
|
+
const matches = entries.filter(entry => provider === 'codex'
|
|
33
|
+
? typeof entry?.pluginId === 'string' && /^simy@[A-Za-z0-9_-]+$/.test(entry.pluginId)
|
|
34
|
+
: entry?.id === CLAUDE_PLUGIN_SELECTOR);
|
|
35
|
+
// Prefer the account installation; report a project-only install explicitly
|
|
36
|
+
// rather than silently treating it as an installation for every project.
|
|
37
|
+
const scopes = ['user', 'managed', 'project', 'local'];
|
|
38
|
+
if (provider === 'claude') matches.sort((a, b) => {
|
|
39
|
+
const rank = value => scopes.includes(value) ? scopes.indexOf(value) : scopes.length;
|
|
40
|
+
return rank(a.scope) - rank(b.scope);
|
|
41
|
+
});
|
|
42
|
+
const entry = matches[0];
|
|
43
|
+
if (!entry || provider === 'codex' && entry.installed === false) return { state: 'missing', installed: false, enabled: false };
|
|
44
|
+
if (typeof entry.enabled !== 'boolean' || provider === 'codex' && entry.installed !== true ||
|
|
45
|
+
provider === 'claude' && !scopes.includes(entry.scope)) return unavailablePluginStatus();
|
|
46
|
+
return { state: entry.enabled ? 'enabled' : 'disabled', installed: true, enabled: entry.enabled,
|
|
47
|
+
...(provider === 'claude' ? { scope: entry.scope, enablementContext: 'home' } : {}) };
|
|
48
|
+
} catch { return unavailablePluginStatus(); }
|
|
49
|
+
};
|
|
50
|
+
const [codex, claude] = await Promise.all([inspect('codex'), inspect('claude')]);
|
|
51
|
+
return { codex, claude };
|
|
52
|
+
}
|