@aerovato/operator-helper 1.6.1 → 1.7.0

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
@@ -32,9 +32,6 @@ operator-helper install pi
32
32
 
33
33
  # DeepSeek Harness
34
34
  operator-helper install deepseek
35
-
36
- # Code Puppy
37
- operator-helper install code-puppy
38
35
  ```
39
36
 
40
37
  Restart the harness, then run `/operator:user-init` in a new conversation to begin setup.
@@ -52,7 +49,6 @@ operator-helper install opencode
52
49
  operator-helper install opencode-v2
53
50
  operator-helper install pi
54
51
  operator-helper install deepseek
55
- operator-helper install code-puppy
56
52
  ```
57
53
 
58
54
  Operator Helper automatically checks for and installs updates when commands run. If it cannot determine whether Bun or npm owns the global installation, it asks for a manual update using the original installation method.
package/dist/index.js CHANGED
@@ -15319,7 +15319,7 @@ var log = (...args) => consoleWith((console2) => sync(() => {
15319
15319
  // package.json
15320
15320
  var package_default = {
15321
15321
  name: "@aerovato/operator-helper",
15322
- version: "1.6.1",
15322
+ version: "1.7.0",
15323
15323
  description: "Installation and setup CLI for Operator Memory",
15324
15324
  license: "BSD-3-Clause",
15325
15325
  author: "Aerovato Research",
@@ -15368,6 +15368,8 @@ var package_default = {
15368
15368
  type: "module"
15369
15369
  };
15370
15370
 
15371
+ // src/commands/common.ts
15372
+ import { lstat, readFile as readFile3 } from "node:fs/promises";
15371
15373
  // src/utils.ts
15372
15374
  function getErrorMessage(error) {
15373
15375
  return error instanceof Error ? error.message : String(error);
@@ -15467,6 +15469,69 @@ function walkFiles(root, directory) {
15467
15469
  });
15468
15470
  }
15469
15471
 
15472
+ // src/commands/common.ts
15473
+ function fileFailure(error) {
15474
+ return { exitCode: 1, output: fileErrorMessage(error) };
15475
+ }
15476
+ function fileErrorMessage(error) {
15477
+ if (error instanceof PathKindError) {
15478
+ return `✗ ${error.path}: Expected a ${error.expected}, found ${error.actual}`;
15479
+ }
15480
+ const path = error.reason instanceof SystemError ? `${error.reason.pathOrDescriptor}: ` : "";
15481
+ return `✗ ${path}${error.reason.description ?? error.message}`;
15482
+ }
15483
+ function runProcess(executable, arguments_, cwd, environment) {
15484
+ return gen2(function* () {
15485
+ const spawner = yield* ChildProcessSpawner;
15486
+ const command = make10(executable, [...arguments_], {
15487
+ cwd,
15488
+ env: environment ?? undefined,
15489
+ extendEnv: true
15490
+ });
15491
+ const execution = yield* scoped2(gen2(function* () {
15492
+ const handle = yield* spawner.spawn(command);
15493
+ const [stdout, stderr, exitCode] = yield* all2([
15494
+ handle.stdout.pipe(decodeText(), mkString),
15495
+ handle.stderr.pipe(decodeText(), mkString),
15496
+ handle.exitCode
15497
+ ], { concurrency: "unbounded" });
15498
+ return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: Number(exitCode) };
15499
+ })).pipe(result2);
15500
+ if (isFailure2(execution)) {
15501
+ const error = execution.failure instanceof PlatformError ? execution.failure.reason.description ?? execution.failure.message : getErrorMessage(execution.failure);
15502
+ return { ok: false, error };
15503
+ }
15504
+ return {
15505
+ ok: true,
15506
+ ...execution.success,
15507
+ output: [execution.success.stdout, execution.success.stderr].filter(Boolean).join(`
15508
+ `)
15509
+ };
15510
+ });
15511
+ }
15512
+ async function pathKind(path) {
15513
+ try {
15514
+ const info = await lstat(path);
15515
+ return info.isFile() ? "file" : info.isDirectory() ? "directory" : "other";
15516
+ } catch (error) {
15517
+ if (isNodeError(error) && error.code === "ENOENT")
15518
+ return "missing";
15519
+ throw error;
15520
+ }
15521
+ }
15522
+ async function readOptionalFile(path) {
15523
+ try {
15524
+ return await readFile3(path, "utf8");
15525
+ } catch (error) {
15526
+ if (isNodeError(error) && error.code === "ENOENT")
15527
+ return null;
15528
+ throw error;
15529
+ }
15530
+ }
15531
+ function isNodeError(error) {
15532
+ return error instanceof Error && "code" in error;
15533
+ }
15534
+
15470
15535
  // src/output.ts
15471
15536
  var import_cli_table3 = __toESM(require_table(), 1);
15472
15537
  var BORDERLESS = {
@@ -15825,9 +15890,6 @@ operator-helper install pi
15825
15890
 
15826
15891
  # Install DeepSeek Harness plugin
15827
15892
  operator-helper install deepseek
15828
-
15829
- # Install Code Puppy plugin
15830
- operator-helper install code-puppy
15831
15893
  \`\`\`
15832
15894
  `;
15833
15895
 
@@ -16153,70 +16215,6 @@ function readTemplate(path) {
16153
16215
  return TEMPLATES[path];
16154
16216
  }
16155
16217
 
16156
- // src/commands/common.ts
16157
- import { lstat, readFile as readFile3 } from "node:fs/promises";
16158
- function fileFailure(error) {
16159
- return { exitCode: 1, output: fileErrorMessage(error) };
16160
- }
16161
- function fileErrorMessage(error) {
16162
- if (error instanceof PathKindError) {
16163
- return `✗ ${error.path}: Expected a ${error.expected}, found ${error.actual}`;
16164
- }
16165
- const path = error.reason instanceof SystemError ? `${error.reason.pathOrDescriptor}: ` : "";
16166
- return `✗ ${path}${error.reason.description ?? error.message}`;
16167
- }
16168
- function runProcess(executable, arguments_, cwd, environment) {
16169
- return gen2(function* () {
16170
- const spawner = yield* ChildProcessSpawner;
16171
- const command = make10(executable, [...arguments_], {
16172
- cwd,
16173
- env: environment ?? undefined,
16174
- extendEnv: true
16175
- });
16176
- const execution = yield* scoped2(gen2(function* () {
16177
- const handle = yield* spawner.spawn(command);
16178
- const [stdout, stderr, exitCode] = yield* all2([
16179
- handle.stdout.pipe(decodeText(), mkString),
16180
- handle.stderr.pipe(decodeText(), mkString),
16181
- handle.exitCode
16182
- ], { concurrency: "unbounded" });
16183
- return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode: Number(exitCode) };
16184
- })).pipe(result2);
16185
- if (isFailure2(execution)) {
16186
- const error = execution.failure instanceof PlatformError ? execution.failure.reason.description ?? execution.failure.message : getErrorMessage(execution.failure);
16187
- return { ok: false, error };
16188
- }
16189
- return {
16190
- ok: true,
16191
- ...execution.success,
16192
- output: [execution.success.stdout, execution.success.stderr].filter(Boolean).join(`
16193
- `)
16194
- };
16195
- });
16196
- }
16197
- async function pathKind(path) {
16198
- try {
16199
- const info = await lstat(path);
16200
- return info.isFile() ? "file" : info.isDirectory() ? "directory" : "other";
16201
- } catch (error) {
16202
- if (isNodeError(error) && error.code === "ENOENT")
16203
- return "missing";
16204
- throw error;
16205
- }
16206
- }
16207
- async function readOptionalFile(path) {
16208
- try {
16209
- return await readFile3(path, "utf8");
16210
- } catch (error) {
16211
- if (isNodeError(error) && error.code === "ENOENT")
16212
- return null;
16213
- throw error;
16214
- }
16215
- }
16216
- function isNodeError(error) {
16217
- return error instanceof Error && "code" in error;
16218
- }
16219
-
16220
16218
  // src/commands/index/init.ts
16221
16219
  var PARTITION_ROOTS = [".operator-shared", ".operator"];
16222
16220
  var indexInit = fn2("indexInit")((context) => gen2(function* () {
@@ -16424,83 +16422,62 @@ var indexLint = fn2("indexLint")(function* (context) {
16424
16422
  };
16425
16423
  });
16426
16424
 
16427
- // src/commands/install/code-puppy.ts
16428
- import { randomUUID } from "node:crypto";
16429
- import { mkdir as mkdir2, readFile as readFile4, rename as rename3, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
16430
- import { join as join2 } from "node:path";
16431
- import { fileURLToPath as fileURLToPath2 } from "node:url";
16432
- var CODE_PUPPY_MARKER = `Operator Memory managed Code Puppy plugin.
16433
- `;
16434
- var CODE_PUPPY_MARKER_FILE = ".operator-managed";
16435
- var installCodePuppy = fn2("installCodePuppy")(function* (context) {
16436
- return yield* promise2(async () => {
16437
- try {
16438
- const source = await readFile4(await codePuppyAdapterPath(), "utf8");
16439
- const plugins = join2(context.home, ".code_puppy", "plugins");
16440
- const target = join2(plugins, "operator");
16441
- const marker = join2(target, CODE_PUPPY_MARKER_FILE);
16442
- const callbacks = join2(target, "register_callbacks.py");
16443
- const targetKind = await pathKind(target);
16444
- if (targetKind === "missing") {
16445
- await mkdir2(plugins, { recursive: true });
16446
- const staged = `${target}.tmp-${randomUUID()}`;
16447
- try {
16448
- await mkdir2(staged);
16449
- await writeFile3(join2(staged, CODE_PUPPY_MARKER_FILE), CODE_PUPPY_MARKER);
16450
- await writeFile3(join2(staged, "register_callbacks.py"), source);
16451
- await rename3(staged, target);
16452
- } finally {
16453
- await rm2(staged, { recursive: true, force: true });
16454
- }
16455
- return { exitCode: 0, output: "✓ Code Puppy plugin installed" };
16456
- }
16457
- if (targetKind !== "directory" || await pathKind(marker) !== "file" || await readOptionalFile(marker) !== CODE_PUPPY_MARKER) {
16458
- return {
16459
- exitCode: 1,
16460
- output: `✗ Preserved unmanaged Code Puppy plugin at ${target}`
16461
- };
16462
- }
16463
- if (await readOptionalFile(callbacks) === source) {
16464
- return { exitCode: 0, output: "✓ Code Puppy plugin is current" };
16465
- }
16466
- const staged = `${callbacks}.tmp-${randomUUID()}`;
16467
- try {
16468
- await writeFile3(staged, source);
16469
- await rename3(staged, callbacks);
16470
- } finally {
16471
- await rm2(staged, { force: true });
16472
- }
16473
- return { exitCode: 0, output: "✓ Code Puppy plugin updated" };
16474
- } catch (error) {
16475
- return {
16476
- exitCode: 1,
16477
- output: `✗ Could not install Code Puppy plugin: ${getErrorMessage(error)}`
16478
- };
16479
- }
16480
- });
16481
- });
16482
- async function codePuppyAdapterPath() {
16483
- const candidates = [
16484
- new URL("./code-puppy/register_callbacks.py", import.meta.url),
16485
- new URL("../../../../code-puppy/operator/register_callbacks.py", import.meta.url)
16425
+ // src/commands/install/claude-code.ts
16426
+ var PLUGIN_ID = "operator@operator-memory";
16427
+ var installClaudeCode = fn2("installClaudeCode")(function* (context) {
16428
+ const helper = yield* runProcess("operator-helper", ["help"], context.cwd, null);
16429
+ const helperFailure = commandFailure(helper, "Operator Helper must be available on PATH");
16430
+ if (helperFailure !== null)
16431
+ return helperFailure;
16432
+ const commands = [
16433
+ ["plugin", "marketplace", "add", "aerovato/operator-memory"],
16434
+ ["plugin", "install", PLUGIN_ID],
16435
+ ["plugin", "update", PLUGIN_ID]
16486
16436
  ];
16487
- for (const candidate of candidates) {
16488
- const path = fileURLToPath2(candidate);
16489
- if (await pathKind(path) === "file") {
16490
- return path;
16491
- }
16437
+ for (const arguments_ of commands) {
16438
+ const result = yield* runProcess("claude", arguments_, context.cwd, null);
16439
+ const failure = commandFailure(result, "Claude Code plugin installation failed");
16440
+ if (failure !== null)
16441
+ return failure;
16442
+ }
16443
+ const status = yield* runProcess("claude", ["plugin", "list", "--json"], context.cwd, null);
16444
+ const statusFailure = commandFailure(status, "Could not verify Claude Code plugin installation");
16445
+ if (statusFailure !== null)
16446
+ return statusFailure;
16447
+ if (!(status.ok && isInstalledAndEnabled(status.stdout))) {
16448
+ return {
16449
+ exitCode: 1,
16450
+ output: "✗ Claude Code plugin installation finished but the plugin is not installed and enabled"
16451
+ };
16452
+ }
16453
+ return {
16454
+ exitCode: 0,
16455
+ output: `✓ Claude Code plugin installed and enabled
16456
+ Start a new Claude Code session.`
16457
+ };
16458
+ });
16459
+ function isInstalledAndEnabled(output) {
16460
+ try {
16461
+ const plugins = JSON.parse(output);
16462
+ return Array.isArray(plugins) && plugins.some((plugin) => typeof plugin === "object" && plugin !== null && ("id" in plugin) && plugin.id === PLUGIN_ID && ("enabled" in plugin) && plugin.enabled === true);
16463
+ } catch {
16464
+ return false;
16492
16465
  }
16493
- throw new Error("bundled Code Puppy adapter is missing");
16466
+ }
16467
+ function commandFailure(result, fallback) {
16468
+ if (!result.ok)
16469
+ return { exitCode: 1, output: `✗ ${fallback}: ${result.error}` };
16470
+ return result.exitCode === 0 ? null : { exitCode: result.exitCode, output: result.output || fallback };
16494
16471
  }
16495
16472
 
16496
16473
  // src/commands/install/codex.ts
16497
- import { mkdir as mkdir3, writeFile as writeFile4 } from "node:fs/promises";
16498
- import { join as join3 } from "node:path";
16474
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
16475
+ import { join as join2 } from "node:path";
16499
16476
  var MARKETPLACE_MARKER = `Operator Memory managed Codex marketplace.
16500
16477
  `;
16501
16478
  var MARKETPLACE_NAME = "operator-memory";
16502
16479
  var PLUGIN_NAME = "aerovato";
16503
- var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
16480
+ var PLUGIN_ID2 = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
16504
16481
  var MARKETPLACE = `${JSON.stringify({
16505
16482
  name: MARKETPLACE_NAME,
16506
16483
  plugins: [
@@ -16524,20 +16501,20 @@ var installCodex = fn2("installCodex")(function* (context) {
16524
16501
  if (!addMarketplace.ok && isMissingBinary(addMarketplace.error)) {
16525
16502
  return yield* registerDesktopMarketplace(context, marketplace.path);
16526
16503
  }
16527
- const addFailure = commandFailure(addMarketplace, "Codex marketplace installation failed");
16504
+ const addFailure = commandFailure2(addMarketplace, "Codex marketplace installation failed");
16528
16505
  if (addFailure !== null)
16529
16506
  return addFailure;
16530
- const install = yield* runProcess("codex", ["plugin", "add", PLUGIN_ID, "--json"], context.cwd, {
16507
+ const install = yield* runProcess("codex", ["plugin", "add", PLUGIN_ID2, "--json"], context.cwd, {
16531
16508
  NPM_CONFIG_MIN_RELEASE_AGE: "0"
16532
16509
  });
16533
- const installFailure = commandFailure(install, "Codex plugin installation failed");
16510
+ const installFailure = commandFailure2(install, "Codex plugin installation failed");
16534
16511
  if (installFailure !== null)
16535
16512
  return installFailure;
16536
16513
  const status = yield* runProcess("codex", ["plugin", "list", "--marketplace", MARKETPLACE_NAME, "--json"], context.cwd, { NPM_CONFIG_MIN_RELEASE_AGE: "0" });
16537
- const statusFailure = commandFailure(status, "Could not verify Codex plugin installation");
16514
+ const statusFailure = commandFailure2(status, "Could not verify Codex plugin installation");
16538
16515
  if (statusFailure !== null)
16539
16516
  return statusFailure;
16540
- if (!(status.ok && isInstalledAndEnabled(status.stdout))) {
16517
+ if (!(status.ok && isInstalledAndEnabled2(status.stdout))) {
16541
16518
  return {
16542
16519
  exitCode: 1,
16543
16520
  output: "✗ Codex plugin installation finished but the plugin is not installed and enabled"
@@ -16554,9 +16531,9 @@ var installCodex = fn2("installCodex")(function* (context) {
16554
16531
  });
16555
16532
  function writeMarketplace(home) {
16556
16533
  return promise2(async () => {
16557
- const root = join3(home, ".operator-helper", "codex-marketplace");
16558
- const marker = join3(root, ".operator-managed");
16559
- const marketplace = join3(root, ".agents", "plugins", "marketplace.json");
16534
+ const root = join2(home, ".operator-helper", "codex-marketplace");
16535
+ const marker = join2(root, ".operator-managed");
16536
+ const marketplace = join2(root, ".agents", "plugins", "marketplace.json");
16560
16537
  try {
16561
16538
  const kind = await pathKind(root);
16562
16539
  if (kind !== "missing" && (kind !== "directory" || await readOptionalFile(marker) !== MARKETPLACE_MARKER)) {
@@ -16568,9 +16545,9 @@ function writeMarketplace(home) {
16568
16545
  }
16569
16546
  };
16570
16547
  }
16571
- await mkdir3(join3(root, ".agents", "plugins"), { recursive: true });
16572
- await writeFile4(marker, MARKETPLACE_MARKER);
16573
- await writeFile4(marketplace, MARKETPLACE);
16548
+ await mkdir2(join2(root, ".agents", "plugins"), { recursive: true });
16549
+ await writeFile3(marker, MARKETPLACE_MARKER);
16550
+ await writeFile3(marketplace, MARKETPLACE);
16574
16551
  return { ok: true, path: root };
16575
16552
  } catch (error) {
16576
16553
  return {
@@ -16588,12 +16565,12 @@ function isMissingBinary(error) {
16588
16565
  }
16589
16566
  function registerDesktopMarketplace(context, marketplacePath) {
16590
16567
  return promise2(async () => {
16591
- const codexHome = process.env.CODEX_HOME || join3(context.home, ".codex");
16592
- const config = join3(codexHome, "config.toml");
16568
+ const codexHome = process.env.CODEX_HOME || join2(context.home, ".codex");
16569
+ const config = join2(codexHome, "config.toml");
16593
16570
  try {
16594
16571
  const content = await readOptionalFile(config);
16595
- await mkdir3(codexHome, { recursive: true });
16596
- await writeFile4(config, upsertMarketplaceTable(content, marketplacePath));
16572
+ await mkdir2(codexHome, { recursive: true });
16573
+ await writeFile3(config, upsertMarketplaceTable(content, marketplacePath));
16597
16574
  } catch (error) {
16598
16575
  return {
16599
16576
  exitCode: 1,
@@ -16645,18 +16622,18 @@ ${block}`;
16645
16622
  `), ...lines.slice(end)].join(`
16646
16623
  `);
16647
16624
  }
16648
- function isInstalledAndEnabled(output) {
16625
+ function isInstalledAndEnabled2(output) {
16649
16626
  try {
16650
16627
  const value = JSON.parse(output);
16651
16628
  if (typeof value !== "object" || value === null || !("installed" in value))
16652
16629
  return false;
16653
16630
  const installed = value.installed;
16654
- return Array.isArray(installed) && installed.some((plugin) => typeof plugin === "object" && plugin !== null && ("pluginId" in plugin) && plugin.pluginId === PLUGIN_ID && ("installed" in plugin) && plugin.installed === true && ("enabled" in plugin) && plugin.enabled === true);
16631
+ return Array.isArray(installed) && installed.some((plugin) => typeof plugin === "object" && plugin !== null && ("pluginId" in plugin) && plugin.pluginId === PLUGIN_ID2 && ("installed" in plugin) && plugin.installed === true && ("enabled" in plugin) && plugin.enabled === true);
16655
16632
  } catch {
16656
16633
  return false;
16657
16634
  }
16658
16635
  }
16659
- function commandFailure(result, fallback) {
16636
+ function commandFailure2(result, fallback) {
16660
16637
  if (!result.ok)
16661
16638
  return { exitCode: 1, output: `✗ Could not run codex: ${result.error}` };
16662
16639
  return result.exitCode === 0 ? null : { exitCode: result.exitCode, output: result.output || fallback };
@@ -16750,7 +16727,7 @@ var installPi = fn2("installPi")(function* (context) {
16750
16727
  import { resolve as resolve2 } from "node:path";
16751
16728
 
16752
16729
  // ../core/src/memory/project.ts
16753
- import { extname, join as join4, relative, sep } from "node:path";
16730
+ import { extname, join as join3, relative, sep } from "node:path";
16754
16731
 
16755
16732
  // ../core/src/utils.ts
16756
16733
  function success(value) {
@@ -16775,17 +16752,17 @@ function readString(value, key) {
16775
16752
 
16776
16753
  // ../core/src/memory/common.ts
16777
16754
  import { constants as constants2 } from "node:fs";
16778
- import { access as access3, readdir, readFile as readFile5, stat as stat3 } from "node:fs/promises";
16755
+ import { access as access3, readdir, readFile as readFile4, stat as stat3 } from "node:fs/promises";
16779
16756
  async function readOptionalContent(path) {
16780
16757
  try {
16781
- return success(await readFile5(path, "utf8"));
16758
+ return success(await readFile4(path, "utf8"));
16782
16759
  } catch (cause) {
16783
16760
  return hasNodeErrorCode(cause, "ENOENT") ? success(null) : failure({ path, cause });
16784
16761
  }
16785
16762
  }
16786
16763
  async function readContent(path) {
16787
16764
  try {
16788
- return success(await readFile5(path, "utf8"));
16765
+ return success(await readFile4(path, "utf8"));
16789
16766
  } catch (cause) {
16790
16767
  return failure({ path, cause });
16791
16768
  }
@@ -16864,13 +16841,13 @@ async function loadProjectPartition(root, loadFiles) {
16864
16841
  return hasNodeErrorCode(directory.error.cause, "ENOENT") ? success(loadFiles ? { exists: false, operatorInstructions: null, catalog: null, indexes: [] } : { exists: false }) : directory;
16865
16842
  }
16866
16843
  if (!loadFiles) {
16867
- for (const path of [join4(root, "operator.md"), join4(root, "catalog.md")]) {
16844
+ for (const path of [join3(root, "operator.md"), join3(root, "catalog.md")]) {
16868
16845
  const file = await checkOptionalReadableFile(path);
16869
16846
  if (!file.ok) {
16870
16847
  return file;
16871
16848
  }
16872
16849
  }
16873
- const indexRoot = join4(root, "index");
16850
+ const indexRoot = join3(root, "index");
16874
16851
  const indexPaths = await listMarkdownFiles(indexRoot, indexRoot);
16875
16852
  if (!indexPaths.ok) {
16876
16853
  return indexPaths;
@@ -16883,15 +16860,15 @@ async function loadProjectPartition(root, loadFiles) {
16883
16860
  }
16884
16861
  return success({ exists: true });
16885
16862
  }
16886
- const operatorInstructions = await readOptionalContent(join4(root, "operator.md"));
16863
+ const operatorInstructions = await readOptionalContent(join3(root, "operator.md"));
16887
16864
  if (!operatorInstructions.ok) {
16888
16865
  return operatorInstructions;
16889
16866
  }
16890
- const catalog = await readOptionalContent(join4(root, "catalog.md"));
16867
+ const catalog = await readOptionalContent(join3(root, "catalog.md"));
16891
16868
  if (!catalog.ok) {
16892
16869
  return catalog;
16893
16870
  }
16894
- const indexes = await readIndexes(join4(root, "index"));
16871
+ const indexes = await readIndexes(join3(root, "index"));
16895
16872
  if (!indexes.ok) {
16896
16873
  return indexes;
16897
16874
  }
@@ -16929,7 +16906,7 @@ async function listMarkdownFiles(root, directory) {
16929
16906
  }
16930
16907
  const paths = [];
16931
16908
  for (const entry of entries.value) {
16932
- const path = join4(directory, entry.name);
16909
+ const path = join3(directory, entry.name);
16933
16910
  if (entry.isDirectory()) {
16934
16911
  const nestedPaths = await listMarkdownFiles(root, path);
16935
16912
  if (!nestedPaths.ok) {
@@ -16950,14 +16927,14 @@ function compareStrings2(left, right) {
16950
16927
  }
16951
16928
 
16952
16929
  // ../core/src/memory/user.ts
16953
- import { join as join5 } from "node:path";
16930
+ import { join as join4 } from "node:path";
16954
16931
  async function loadUserPartition(root, loadFiles) {
16955
16932
  if (!loadFiles) {
16956
16933
  const directory = await readDirectory2(root);
16957
16934
  if (!directory.ok) {
16958
16935
  return hasNodeErrorCode(directory.error.cause, "ENOENT") ? success({ exists: false }) : directory;
16959
16936
  }
16960
- for (const path of [join5(root, "operator.md"), join5(root, "catalog.md")]) {
16937
+ for (const path of [join4(root, "operator.md"), join4(root, "catalog.md")]) {
16961
16938
  const file = await checkOptionalReadableFile(path);
16962
16939
  if (!file.ok) {
16963
16940
  return file;
@@ -16965,11 +16942,11 @@ async function loadUserPartition(root, loadFiles) {
16965
16942
  }
16966
16943
  return success({ exists: true });
16967
16944
  }
16968
- const operatorInstructions = await readOptionalContent(join5(root, "operator.md"));
16945
+ const operatorInstructions = await readOptionalContent(join4(root, "operator.md"));
16969
16946
  if (!operatorInstructions.ok) {
16970
16947
  return operatorInstructions;
16971
16948
  }
16972
- const catalog = await readOptionalContent(join5(root, "catalog.md"));
16949
+ const catalog = await readOptionalContent(join4(root, "catalog.md"));
16973
16950
  if (!catalog.ok) {
16974
16951
  return catalog;
16975
16952
  }
@@ -17647,103 +17624,6 @@ var describeFile = fn2("describeFile")(function* (cwd, root, file, label) {
17647
17624
  ];
17648
17625
  });
17649
17626
 
17650
- // src/commands/user/common.ts
17651
- var USER_ROOT = "~/.operator/user";
17652
- var USER_FILES = ["operator.md", "catalog.md"];
17653
-
17654
- // src/commands/user/init.ts
17655
- var userInit = fn2("userInit")((context) => gen2(function* () {
17656
- const pathService = yield* Path;
17657
- const root = pathService.join(context.home, ".operator", "user");
17658
- yield* ensurePrivateDirectory(pathService.join(context.home, ".operator"));
17659
- const templates = {
17660
- "operator.md": readTemplate("user/operator.md" /* UserOperator */),
17661
- "catalog.md": readTemplate("user/catalog.md" /* UserCatalog */)
17662
- };
17663
- const rows = [];
17664
- let failed = false;
17665
- for (const file of USER_FILES) {
17666
- const written = yield* writeTextIfMissing(pathService.join(root, file), templates[file]).pipe(result2);
17667
- if (isFailure2(written)) {
17668
- failed = true;
17669
- rows.push(["Failed", `${USER_ROOT}/${file}: ${fileErrorMessage(written.failure)}`]);
17670
- } else {
17671
- rows.push([
17672
- written.success === "created" ? "Created" : "Preserved",
17673
- `${USER_ROOT}/${file}`
17674
- ]);
17675
- }
17676
- }
17677
- return {
17678
- exitCode: failed ? 1 : 0,
17679
- output: `✓ User Partition Initialized
17680
- ${renderTable(rows)}`
17681
- };
17682
- }).pipe(catch_2((error) => succeed6(fileFailure(error))), map3((result) => ({
17683
- ...result,
17684
- output: `${result.output}
17685
-
17686
- Follow the agent instructions below to complete User Setup.
17687
-
17688
- ${readTemplate("user/setup.md" /* UserSetup */)}`
17689
- }))));
17690
-
17691
- // src/commands/user/status.ts
17692
- var INIT_HINT = [
17693
- "For Users: Run `/operator:user-init` in your harness to initialize.",
17694
- "For Agents: Run `operator-helper user init` and follow the guide in its output."
17695
- ];
17696
- var userStatus = fn2("userStatus")(function* (context) {
17697
- const pathService = yield* Path;
17698
- const root = pathService.join(context.home, ".operator", "user");
17699
- const kind = yield* inspectPath(root);
17700
- if (kind === "missing") {
17701
- return {
17702
- exitCode: 0,
17703
- output: [
17704
- `User Partition: ${USER_ROOT}`,
17705
- "",
17706
- "⚠ User Partition Missing",
17707
- renderTable([["Expected Location", `${USER_ROOT}/`]]),
17708
- "",
17709
- ...INIT_HINT
17710
- ].join(`
17711
- `)
17712
- };
17713
- }
17714
- if (kind !== "directory") {
17715
- return yield* new PathKindError({
17716
- path: root,
17717
- expected: "directory",
17718
- actual: kind
17719
- });
17720
- }
17721
- const rows = [];
17722
- let missingCore = false;
17723
- for (const [file, label] of [
17724
- ["operator.md", "User Instructions"],
17725
- ["catalog.md", "User Catalog"]
17726
- ]) {
17727
- const content = yield* readOptionalText(pathService.join(root, file));
17728
- if (content === null) {
17729
- missingCore = true;
17730
- }
17731
- rows.push([
17732
- label,
17733
- content === null ? `Missing (${USER_ROOT}/${file})` : `${USER_ROOT}/${file} (${Buffer.byteLength(content)} bytes)`
17734
- ]);
17735
- }
17736
- const files = yield* listFiles(root);
17737
- const freeform = files.filter((file) => file.relativePath !== "operator.md" && file.relativePath !== "catalog.md");
17738
- rows.push(["Freeform Content", `${freeform.length} files`]);
17739
- const lines = [`User Partition: ${USER_ROOT}`, "", "✓ User Partition Found", renderTable(rows)];
17740
- if (missingCore) {
17741
- lines.push("", ...INIT_HINT);
17742
- }
17743
- return { exitCode: 0, output: lines.join(`
17744
- `) };
17745
- });
17746
-
17747
17627
  // src/update.ts
17748
17628
  import { realpathSync } from "node:fs";
17749
17629
  import { sep as sep2 } from "node:path";
@@ -17897,6 +17777,103 @@ var upgrade = fn2("upgrade")(function* (context) {
17897
17777
  }
17898
17778
  });
17899
17779
 
17780
+ // src/commands/user/common.ts
17781
+ var USER_ROOT = "~/.operator/user";
17782
+ var USER_FILES = ["operator.md", "catalog.md"];
17783
+
17784
+ // src/commands/user/init.ts
17785
+ var userInit = fn2("userInit")((context) => gen2(function* () {
17786
+ const pathService = yield* Path;
17787
+ const root = pathService.join(context.home, ".operator", "user");
17788
+ yield* ensurePrivateDirectory(pathService.join(context.home, ".operator"));
17789
+ const templates = {
17790
+ "operator.md": readTemplate("user/operator.md" /* UserOperator */),
17791
+ "catalog.md": readTemplate("user/catalog.md" /* UserCatalog */)
17792
+ };
17793
+ const rows = [];
17794
+ let failed = false;
17795
+ for (const file of USER_FILES) {
17796
+ const written = yield* writeTextIfMissing(pathService.join(root, file), templates[file]).pipe(result2);
17797
+ if (isFailure2(written)) {
17798
+ failed = true;
17799
+ rows.push(["Failed", `${USER_ROOT}/${file}: ${fileErrorMessage(written.failure)}`]);
17800
+ } else {
17801
+ rows.push([
17802
+ written.success === "created" ? "Created" : "Preserved",
17803
+ `${USER_ROOT}/${file}`
17804
+ ]);
17805
+ }
17806
+ }
17807
+ return {
17808
+ exitCode: failed ? 1 : 0,
17809
+ output: `✓ User Partition Initialized
17810
+ ${renderTable(rows)}`
17811
+ };
17812
+ }).pipe(catch_2((error) => succeed6(fileFailure(error))), map3((result) => ({
17813
+ ...result,
17814
+ output: `${result.output}
17815
+
17816
+ Follow the agent instructions below to complete User Setup.
17817
+
17818
+ ${readTemplate("user/setup.md" /* UserSetup */)}`
17819
+ }))));
17820
+
17821
+ // src/commands/user/status.ts
17822
+ var INIT_HINT = [
17823
+ "For Users: Run `/operator:user-init` in your harness to initialize.",
17824
+ "For Agents: Run `operator-helper user init` and follow the guide in its output."
17825
+ ];
17826
+ var userStatus = fn2("userStatus")(function* (context) {
17827
+ const pathService = yield* Path;
17828
+ const root = pathService.join(context.home, ".operator", "user");
17829
+ const kind = yield* inspectPath(root);
17830
+ if (kind === "missing") {
17831
+ return {
17832
+ exitCode: 0,
17833
+ output: [
17834
+ `User Partition: ${USER_ROOT}`,
17835
+ "",
17836
+ "⚠ User Partition Missing",
17837
+ renderTable([["Expected Location", `${USER_ROOT}/`]]),
17838
+ "",
17839
+ ...INIT_HINT
17840
+ ].join(`
17841
+ `)
17842
+ };
17843
+ }
17844
+ if (kind !== "directory") {
17845
+ return yield* new PathKindError({
17846
+ path: root,
17847
+ expected: "directory",
17848
+ actual: kind
17849
+ });
17850
+ }
17851
+ const rows = [];
17852
+ let missingCore = false;
17853
+ for (const [file, label] of [
17854
+ ["operator.md", "User Instructions"],
17855
+ ["catalog.md", "User Catalog"]
17856
+ ]) {
17857
+ const content = yield* readOptionalText(pathService.join(root, file));
17858
+ if (content === null) {
17859
+ missingCore = true;
17860
+ }
17861
+ rows.push([
17862
+ label,
17863
+ content === null ? `Missing (${USER_ROOT}/${file})` : `${USER_ROOT}/${file} (${Buffer.byteLength(content)} bytes)`
17864
+ ]);
17865
+ }
17866
+ const files = yield* listFiles(root);
17867
+ const freeform = files.filter((file) => file.relativePath !== "operator.md" && file.relativePath !== "catalog.md");
17868
+ rows.push(["Freeform Content", `${freeform.length} files`]);
17869
+ const lines = [`User Partition: ${USER_ROOT}`, "", "✓ User Partition Found", renderTable(rows)];
17870
+ if (missingCore) {
17871
+ lines.push("", ...INIT_HINT);
17872
+ }
17873
+ return { exitCode: 0, output: lines.join(`
17874
+ `) };
17875
+ });
17876
+
17900
17877
  // src/commands/version.ts
17901
17878
  var version = fn2("version")(function* (context) {
17902
17879
  const latest = yield* checkForUpdate(context.version).pipe(catch_2((error) => succeed6({ status: "failed", message: error.message })));
@@ -17928,7 +17905,7 @@ ${renderTable([
17928
17905
  ["install opencode-v2", "Install or update the OpenCode V2 plugin"],
17929
17906
  ["install deepseek", "Install or update the DeepSeek Harness plugin"],
17930
17907
  ["install pi", "Install or update the Pi plugin"],
17931
- ["install code-puppy", "Install or update the Code Puppy plugin"]
17908
+ ["install claude-code", "Install or update the Claude Code plugin"]
17932
17909
  ])}
17933
17910
 
17934
17911
  AGENT COMMANDS
@@ -17979,6 +17956,8 @@ function runCli(arguments_, context) {
17979
17956
  return yield* memoryCheck(context);
17980
17957
  case "install codex":
17981
17958
  return yield* installCodex(context);
17959
+ case "install claude-code":
17960
+ return yield* installClaudeCode(context);
17982
17961
  case "install opencode":
17983
17962
  return yield* installOpenCode(context);
17984
17963
  case "install opencode-v2":
@@ -17987,8 +17966,6 @@ function runCli(arguments_, context) {
17987
17966
  return yield* installDeepSeek(context);
17988
17967
  case "install pi":
17989
17968
  return yield* installPi(context);
17990
- case "install code-puppy":
17991
- return yield* installCodePuppy(context);
17992
17969
  case "user guide":
17993
17970
  case "project guide":
17994
17971
  case "index guide":
@@ -18027,4 +18004,4 @@ var main = gen2(function* () {
18027
18004
  }).pipe(provide4(LiveLayers));
18028
18005
  runMain2(main);
18029
18006
 
18030
- //# debugId=EB111CCB847C680664756E2164756E21
18007
+ //# debugId=FB1D99D1EABA3A2364756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aerovato/operator-helper",
3
- "version": "1.6.1",
3
+ "version": "1.7.0",
4
4
  "description": "Installation and setup CLI for Operator Memory",
5
5
  "license": "BSD-3-Clause",
6
6
  "author": "Aerovato Research",
@@ -1,407 +0,0 @@
1
- """Operator Memory integration for Code Puppy."""
2
-
3
- from __future__ import annotations
4
-
5
- import asyncio
6
- import importlib.metadata
7
- import os
8
- import subprocess
9
- import threading
10
- from collections.abc import AsyncGenerator
11
- from contextlib import asynccontextmanager, suppress
12
- from typing import Any
13
-
14
- from code_puppy.callbacks import register_callback
15
- from code_puppy.config import get_current_session_name
16
- from code_puppy.messaging import emit_warning
17
- from code_puppy.tools.subagent_context import get_conversation_root_id
18
- from pydantic_ai import ModelRequest, RunContext, UserPromptPart
19
- from pydantic_ai.messages import ModelMessage, ModelResponse
20
- from pydantic_ai.models import (
21
- KnownModelName,
22
- Model,
23
- ModelRequestParameters,
24
- StreamedResponse,
25
- )
26
- from pydantic_ai.models.wrapper import WrapperModel
27
- from pydantic_ai.settings import ModelSettings
28
-
29
- _MARKER_KEY = "operator_memory"
30
- _MARKER_VALUE = "synthetic-preamble-v1"
31
- _MODEL_MESSAGE_TRANSFORM_VERSION = (0, 0, 753)
32
-
33
- try:
34
- from code_puppy.callbacks import CustomCommandResult
35
- except ImportError:
36
- try:
37
- # Code Puppy < 0.0.698 exposes the same contract in the
38
- # customizable-commands plugin.
39
- from code_puppy.plugins.customizable_commands.register_callbacks import (
40
- MarkdownCommandResult as CustomCommandResult,
41
- )
42
- except ImportError:
43
- CustomCommandResult = None
44
-
45
- _RENDER_TIMEOUT_SECONDS = 30
46
- _render_tasks: dict[str, asyncio.Task[str]] = {}
47
- _update_launched = False
48
-
49
- _COMMANDS = {
50
- "operator:user-init": (
51
- "Initialize Operator User Instructions",
52
- "user init",
53
- "Follow the User Setup guide in its output, including when initialization reports failures.",
54
- ),
55
- "operator:project-init": (
56
- "Initialize Operator Project",
57
- "project init",
58
- "Follow the Project Setup guide in its output, including when initialization reports failures.",
59
- ),
60
- "operator:index": (
61
- "Build or refresh the Operator Project Index",
62
- "index init",
63
- "Follow the Project Index Setup guide in its output, including when inspection reports failures.",
64
- ),
65
- "operator:repair": (
66
- "Repair Operator",
67
- "memory check",
68
- "If no issues are detected, stop. Otherwise, repair only the reported load "
69
- "failures without initializing absent partitions. Rerun `operator-helper memory "
70
- "check` to confirm the repair, then read the applicable Operator memory documents.",
71
- ),
72
- }
73
-
74
-
75
- class OperatorModel(WrapperModel):
76
- """Inject an immutable Operator preamble into final model requests."""
77
-
78
- def __init__(self, wrapped: Model[Any] | KnownModelName, conversation_key: str):
79
- super().__init__(wrapped)
80
- self._conversation_key = conversation_key
81
-
82
- async def request(
83
- self,
84
- messages: list[ModelMessage],
85
- model_settings: ModelSettings | None,
86
- model_request_parameters: ModelRequestParameters,
87
- ) -> ModelResponse:
88
- transformed = await _inject_preamble(messages, self._conversation_key)
89
- return await self.wrapped.request(
90
- transformed, model_settings, model_request_parameters
91
- )
92
-
93
- @asynccontextmanager
94
- async def request_stream(
95
- self,
96
- messages: list[ModelMessage],
97
- model_settings: ModelSettings | None,
98
- model_request_parameters: ModelRequestParameters,
99
- run_context: RunContext[Any] | None = None,
100
- ) -> AsyncGenerator[StreamedResponse]:
101
- transformed = await _inject_preamble(messages, self._conversation_key)
102
- async with self.wrapped.request_stream(
103
- transformed, model_settings, model_request_parameters, run_context
104
- ) as response:
105
- yield response
106
-
107
-
108
- async def _inject_preamble(
109
- messages: list[ModelMessage], conversation_key: str
110
- ) -> list[ModelMessage]:
111
- preamble = await _get_preamble(conversation_key)
112
- transformed = [message for message in messages if not _is_operator_request(message)]
113
- transformed.insert(
114
- 0,
115
- ModelRequest(
116
- parts=[UserPromptPart(content=preamble)],
117
- metadata={_MARKER_KEY: _MARKER_VALUE},
118
- ),
119
- )
120
- return transformed
121
-
122
-
123
- async def _transform_model_messages(
124
- _agent_name: str | None, messages: list[ModelMessage]
125
- ) -> None:
126
- messages[:] = await _inject_preamble(messages, _conversation_key())
127
-
128
-
129
- def _is_operator_request(message: ModelMessage) -> bool:
130
- return (
131
- isinstance(message, ModelRequest)
132
- and message.metadata is not None
133
- and message.metadata.get(_MARKER_KEY) == _MARKER_VALUE
134
- )
135
-
136
-
137
- async def _get_preamble(conversation_key: str) -> str:
138
- task = _render_tasks.get(conversation_key)
139
- if task is None:
140
- task = asyncio.create_task(_render_preamble(conversation_key))
141
- _render_tasks[conversation_key] = task
142
- return await asyncio.shield(task)
143
-
144
-
145
- async def _render_preamble(conversation_key: str) -> str:
146
- process: asyncio.subprocess.Process | None = None
147
- try:
148
- process = await asyncio.create_subprocess_exec(
149
- "operator-helper",
150
- "preamble",
151
- stdout=asyncio.subprocess.PIPE,
152
- stderr=asyncio.subprocess.PIPE,
153
- )
154
- stdout, stderr = await asyncio.wait_for(
155
- process.communicate(), timeout=_RENDER_TIMEOUT_SECONDS
156
- )
157
- content = _remove_process_newline(stdout.decode(errors="replace"))
158
- if process.returncode == 0 and content:
159
- return content
160
- detail = _remove_process_newline(stderr.decode(errors="replace"))
161
- reason = detail or f"operator-helper exited with status {process.returncode}"
162
- except asyncio.CancelledError:
163
- if process is not None:
164
- await _stop_process(process)
165
- raise
166
- except TimeoutError:
167
- if process is not None:
168
- await _stop_process(process)
169
- reason = "operator-helper preamble timed out"
170
- except OSError as error:
171
- if process is not None:
172
- await _stop_process(process)
173
- reason = str(error)
174
-
175
- emit_warning(
176
- "Operator Memory could not render for this conversation. "
177
- "Run `operator-helper install code-puppy` to repair the integration."
178
- )
179
- return (
180
- "<operator-diagnostic>\n"
181
- "Operator Memory could not render its preamble. Continue without Operator Memory, "
182
- "help the user repair `operator-helper`, then ask them to start a new conversation.\n"
183
- f"Failure: {reason}\n"
184
- "</operator-diagnostic>"
185
- )
186
-
187
-
188
- async def _stop_process(process: asyncio.subprocess.Process) -> None:
189
- with suppress(ProcessLookupError):
190
- process.kill()
191
- await process.wait()
192
-
193
-
194
- def _remove_process_newline(value: str) -> str:
195
- if value.endswith("\r\n"):
196
- return value[:-2]
197
- if value.endswith("\n"):
198
- return value[:-1]
199
- return value
200
-
201
-
202
- def _conversation_key() -> str:
203
- return get_conversation_root_id() or get_current_session_name()
204
-
205
-
206
- def _installed_code_puppy_version() -> str | None:
207
- try:
208
- return importlib.metadata.version("code-puppy")
209
- except Exception:
210
- return None
211
-
212
-
213
- def _supports_model_message_transform(version: str | None) -> bool:
214
- if version is None:
215
- return False
216
- release = version.partition("+")[0].partition("-")[0]
217
- try:
218
- parts = tuple(int(part) for part in release.split("."))
219
- except ValueError:
220
- return False
221
- return parts >= _MODEL_MESSAGE_TRANSFORM_VERSION
222
-
223
-
224
- def _register_preamble_injection(version: str | None) -> None:
225
- if _supports_model_message_transform(version):
226
- register_callback("transform_model_messages", _transform_model_messages)
227
- else:
228
- register_callback("agent_run_context", _agent_run_context)
229
-
230
-
231
- @asynccontextmanager
232
- async def _agent_run_context(
233
- _agent: Any, pydantic_agent: Any, _group_id: str, _mcp_servers: Any
234
- ) -> AsyncGenerator[None]:
235
- model = pydantic_agent.model
236
- if model is None:
237
- yield
238
- return
239
- if _is_dbos_agent(pydantic_agent):
240
- _warn_dbos_incompatible()
241
- yield
242
- return
243
- with pydantic_agent.override(model=OperatorModel(model, _conversation_key())):
244
- yield
245
-
246
-
247
- def _is_dbos_agent(pydantic_agent: Any) -> bool:
248
- # DBOSAgent installs its own captured DBOSModel through a nested
249
- # override inside run(), which supersedes any override applied here.
250
- # Duck-type: avoid importing the optional durable_exec module.
251
- return hasattr(pydantic_agent, "_dbos_overrides")
252
-
253
-
254
- _DBOS_WARNED = False
255
-
256
-
257
- def _warn_dbos_incompatible() -> None:
258
- global _DBOS_WARNED
259
- if _DBOS_WARNED:
260
- return
261
- _DBOS_WARNED = True
262
- emit_warning(
263
- "Operator Memory preamble injection is disabled: DBOS durable execution "
264
- "supersedes the model override it relies on. Disable DBOS to restore "
265
- "Operator Memory: /set enable_dbos false, then restart Code Puppy."
266
- )
267
-
268
-
269
- async def _session_end() -> None:
270
- tasks = tuple(_render_tasks.values())
271
- _render_tasks.clear()
272
- for task in tasks:
273
- task.cancel()
274
- await asyncio.gather(*tasks, return_exceptions=True)
275
-
276
-
277
- def _custom_command(_command: str, name: str) -> CustomCommandResult | None:
278
- definition = _COMMANDS.get(name)
279
- if definition is None:
280
- return None
281
-
282
- description, operation, instructions = definition
283
- return CustomCommandResult(
284
- f"# {description}\n\n"
285
- "1. Run `operator-helper version`. If an update is available, run "
286
- "`operator-helper upgrade` before continuing.\n"
287
- f"2. Run `operator-helper {operation}`. {instructions}\n\n"
288
- "## Recovery\n\n"
289
- "- If Helper cannot start, install or repair the npm package "
290
- "`@aerovato/operator-helper` globally and retry the failed command.\n"
291
- "- If the version check or upgrade fails, diagnose the error and retry.\n"
292
- f"- If `operator-helper {operation}` reports a failure, use its output to "
293
- "resolve it and rerun it as needed.\n"
294
- "- If you cannot resolve a problem, report the blocker.\n\n"
295
- "Use Helper output as working context. Do not reproduce it wholesale or "
296
- "reimplement Helper logic."
297
- )
298
-
299
-
300
- def _custom_command_help() -> list[tuple[str, str]]:
301
- return [(name, definition[0]) for name, definition in _COMMANDS.items()]
302
-
303
-
304
- _UPDATE_NOTE = "Operator Updated · Restart Code Puppy to apply"
305
- _update_note_pending = False
306
-
307
-
308
- def _launch_update() -> None:
309
- global _update_launched
310
- if _update_launched:
311
- return
312
- _update_launched = True
313
- try:
314
- process = subprocess.Popen(
315
- ("operator-helper", "install", "code-puppy"),
316
- stdin=subprocess.DEVNULL,
317
- stdout=subprocess.PIPE,
318
- stderr=subprocess.DEVNULL,
319
- text=True,
320
- creationflags=(
321
- subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
322
- if os.name == "nt"
323
- else 0
324
- ),
325
- start_new_session=os.name != "nt",
326
- )
327
- except OSError:
328
- return
329
- threading.Thread(target=_monitor_update, args=(process,), daemon=True).start()
330
-
331
-
332
- def _monitor_update(process: subprocess.Popen[str]) -> None:
333
- global _update_note_pending
334
- try:
335
- output = process.communicate()[0] or ""
336
- except Exception:
337
- return
338
- if process.returncode == 0 and "plugin updated" in output:
339
- _update_note_pending = True
340
- _paint_status_indicator(_UPDATE_NOTE)
341
- _emit_update_notice()
342
-
343
-
344
- def _emit_update_notice() -> None:
345
- try:
346
- from rich.text import Text
347
-
348
- from code_puppy.messaging import emit_warning
349
-
350
- notice = Text()
351
- notice.append("Operator Updated · ", style="bold magenta")
352
- notice.append("Restart Code Puppy to apply", style="magenta")
353
- emit_warning(notice)
354
- except Exception:
355
- pass
356
-
357
-
358
- _STARTUP_INDICATOR = "Operator Ready"
359
- _STATUS_INDICATOR = " Operator Ready"
360
- _active_runs = 0
361
-
362
-
363
- def _paint_status_indicator(text: str) -> None:
364
- try:
365
- from code_puppy.messaging.bottom_bar import get_bottom_bar
366
-
367
- get_bottom_bar().set_status_suffix(text)
368
- except Exception:
369
- pass
370
-
371
-
372
- async def _show_status_indicator() -> None:
373
- global _update_note_pending
374
- if _update_note_pending:
375
- _update_note_pending = False
376
- return
377
- _paint_status_indicator(_STARTUP_INDICATOR)
378
-
379
-
380
- async def _agent_run_start(*_args: Any) -> None:
381
- global _active_runs
382
- _active_runs += 1
383
- _paint_status_indicator(_STATUS_INDICATOR)
384
-
385
-
386
- async def _agent_run_end(*_args: Any) -> None:
387
- global _active_runs
388
- _active_runs = max(0, _active_runs - 1)
389
- if _active_runs == 0:
390
- _paint_status_indicator("")
391
-
392
-
393
- _register_preamble_injection(_installed_code_puppy_version())
394
- register_callback("session_end", _session_end)
395
- if CustomCommandResult is not None:
396
- register_callback("custom_command", _custom_command)
397
- register_callback("custom_command_help", _custom_command_help)
398
- else:
399
- emit_warning(
400
- "Operator commands are disabled: this Code Puppy version is too old to "
401
- "process command output as user input. Preamble injection still works. "
402
- "Upgrade Code Puppy to enable the /operator:* commands."
403
- )
404
- register_callback("startup", _show_status_indicator)
405
- register_callback("agent_run_start", _agent_run_start)
406
- register_callback("agent_run_end", _agent_run_end)
407
- _launch_update()