@awak-app/simy-cli 0.6.1 → 0.6.4

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.
@@ -0,0 +1,43 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { deliveryStop } from '../codex-plugins/simy/hooks/delivery-loop.mjs';
3
+
4
+ // Auto-continue works while nobody is watching, so a person must learn when it
5
+ // stops. Map a stopped run to a notification kind; the native window owns copy.
6
+ export function stopNotificationKind(run, now = Date.now()) {
7
+ if (!run?.managed?.autoContinueEpoch) return null;
8
+ // The clock keeps aging while paused; the persisted human decision takes
9
+ // precedence over a threshold that was reached only after that decision.
10
+ if (run.state === 'paused') return run.reason === 'Paused by user.' ? null : deliveryStop(run, now) ? 'paused' : null;
11
+ if (run.state === 'blocked' || run.state === 'verification_pending') return 'needs_decision';
12
+ if (run.state === 'recovery_required' && run.managed.autoRecovery !== true) return 'needs_decision';
13
+ if (run.state === 'failed' || run.state === 'budget_exhausted') return 'failed';
14
+ if (run.state === 'completed' || run.state === 'reported_done') return 'finished';
15
+ return null;
16
+ }
17
+
18
+ // The first observation is a baseline: restarting SIMY must not replay alerts
19
+ // for runs that stopped earlier. Each distinct stop is announced once.
20
+ export function createStopNotifier({ emit, now = Date.now } = {}) {
21
+ // Retain one stop fingerprint per currently observed task, rather than an
22
+ // event cache whose eviction could replay the entire historical baseline.
23
+ const seen = new Map();
24
+ let baseline = true;
25
+ return {
26
+ observe(records) {
27
+ const present = new Set();
28
+ for (const record of records) {
29
+ const run = record?.run, kind = stopNotificationKind(run, now());
30
+ const key = JSON.stringify([record.provider, record.session, run?.managed?.taskId]);
31
+ present.add(key);
32
+ if (!kind) { seen.delete(key); continue; }
33
+ const id = createHash('sha256').update(JSON.stringify([record.provider, record.session, run.managed.taskId, run.state, run.revision, run.reason])).digest('hex').slice(0, 32);
34
+ if (seen.get(key) === id) continue;
35
+ seen.set(key, id);
36
+ if (!baseline) emit({ type: 'notify', id, kind, provider: record.provider, session: record.session,
37
+ objective: String(run.objective || '').slice(0, 200) });
38
+ }
39
+ for (const key of seen.keys()) if (!present.has(key)) seen.delete(key);
40
+ baseline = false;
41
+ },
42
+ };
43
+ }
@@ -3,6 +3,7 @@ import { cp, lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { hookRuntimeCommand } from "../hook-runtime.js";
6
7
 
7
8
  import { inspectClaudePlugin, installClaudePlugin } from "./claude-plugin.js";
8
9
 
@@ -328,25 +329,14 @@ async function bundledPluginManifest({ overrides = new Map() } = {}) {
328
329
  async function installedPluginOverrides() {
329
330
  const relative = "hooks/hooks.json";
330
331
  const source = JSON.parse(await readFile(path.join(BUNDLED_CODEX_PLUGIN_ROOT, relative), "utf8"));
331
- const commandPrefix = "node ";
332
- const runtime = shellQuote(process.execPath);
333
332
  for (const groups of Object.values(source.hooks || {})) {
334
333
  for (const group of groups || []) {
335
- for (const hook of group.hooks || []) {
336
- if (typeof hook.command === "string" && hook.command.startsWith(commandPrefix)) {
337
- hook.command = `${runtime} ${hook.command.slice(commandPrefix.length)}`;
338
- }
339
- }
334
+ for (const hook of group.hooks || []) hook.command = hookRuntimeCommand(hook.command);
340
335
  }
341
336
  }
342
337
  return new Map([[relative, `${JSON.stringify(source, null, 2)}\n`]]);
343
338
  }
344
339
 
345
- function shellQuote(value) {
346
- if (process.platform === "win32") return `"${value.replaceAll('"', '""')}"`;
347
- return `'${value.replaceAll("'", "'\\''")}'`;
348
- }
349
-
350
340
  async function directoryDigest(root, { missing = "throw", manifest, overrides = new Map() } = {}) {
351
341
  let rootStat;
352
342
  try {
@@ -3,6 +3,7 @@ import { mkdir, readFile, readdir, rename, rm, lstat, writeFile } from "node:fs/
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { hookRuntimeCommand } from "../hook-runtime.js";
6
7
 
7
8
  const SOURCE = fileURLToPath(new URL("../codex-plugins/simy/", import.meta.url));
8
9
  export const CLAUDE_MARKETPLACE = "simy-local";
@@ -43,7 +44,7 @@ export async function claudePluginFiles() {
43
44
  } else if (event === "Notification") {
44
45
  hook.statusMessage = "SIMY Delivery Loop permission-notification tracking";
45
46
  }
46
- hook.command = hook.command.replace(/^node /, `${quoteRuntime(process.execPath)} `)
47
+ hook.command = hookRuntimeCommand(hook.command)
47
48
  .replaceAll("process.env.PLUGIN_ROOT", "process.env.CLAUDE_PLUGIN_ROOT")
48
49
  .replaceAll("hook.main('codex')", "hook.main('claude')");
49
50
  }
@@ -136,6 +137,3 @@ function json(value) { return `${JSON.stringify(value, null, 2)}\n`; }
136
137
  async function optionalRead(file) {
137
138
  try { return await readFile(file, "utf8"); } catch (error) { if (error.code === "ENOENT") return null; throw error; }
138
139
  }
139
- function quoteRuntime(value) {
140
- return process.platform === "win32" ? `"${value.replaceAll('"', '""')}"` : `'${value.replaceAll("'", "'\\''")}'`;
141
- }