@aerovato/operator-helper 1.6.1 → 1.7.1

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.1",
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 = {
@@ -15811,23 +15876,23 @@ npm install --global @aerovato/operator-helper
15811
15876
  # With Bun
15812
15877
  bun add --global --minimum-release-age 0 @aerovato/operator-helper@latest
15813
15878
 
15814
- # Install OpenCode plugin
15815
- operator-helper install opencode
15879
+ # Install Claude Code plugin
15880
+ operator-helper install claude-code
15881
+
15882
+ # Install Codex plugin
15883
+ operator-helper install codex
15816
15884
 
15817
15885
  # Install OpenCode V2 plugin
15818
15886
  operator-helper install opencode-v2
15819
15887
 
15820
- # Install Codex plugin
15821
- operator-helper install codex
15888
+ # Install OpenCode plugin
15889
+ operator-helper install opencode
15822
15890
 
15823
15891
  # Install Pi plugin
15824
15892
  operator-helper install pi
15825
15893
 
15826
15894
  # Install DeepSeek Harness plugin
15827
15895
  operator-helper install deepseek
15828
-
15829
- # Install Code Puppy plugin
15830
- operator-helper install code-puppy
15831
15896
  \`\`\`
15832
15897
  `;
15833
15898
 
@@ -16153,70 +16218,6 @@ function readTemplate(path) {
16153
16218
  return TEMPLATES[path];
16154
16219
  }
16155
16220
 
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
16221
  // src/commands/index/init.ts
16221
16222
  var PARTITION_ROOTS = [".operator-shared", ".operator"];
16222
16223
  var indexInit = fn2("indexInit")((context) => gen2(function* () {
@@ -16424,83 +16425,62 @@ var indexLint = fn2("indexLint")(function* (context) {
16424
16425
  };
16425
16426
  });
16426
16427
 
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)
16428
+ // src/commands/install/claude-code.ts
16429
+ var PLUGIN_ID = "operator@operator-memory";
16430
+ var installClaudeCode = fn2("installClaudeCode")(function* (context) {
16431
+ const helper = yield* runProcess("operator-helper", ["help"], context.cwd, null);
16432
+ const helperFailure = commandFailure(helper, "Operator Helper must be available on PATH");
16433
+ if (helperFailure !== null)
16434
+ return helperFailure;
16435
+ const commands = [
16436
+ ["plugin", "marketplace", "add", "aerovato/operator-memory"],
16437
+ ["plugin", "install", PLUGIN_ID],
16438
+ ["plugin", "update", PLUGIN_ID]
16486
16439
  ];
16487
- for (const candidate of candidates) {
16488
- const path = fileURLToPath2(candidate);
16489
- if (await pathKind(path) === "file") {
16490
- return path;
16491
- }
16440
+ for (const arguments_ of commands) {
16441
+ const result = yield* runProcess("claude", arguments_, context.cwd, null);
16442
+ const failure = commandFailure(result, "Claude Code plugin installation failed");
16443
+ if (failure !== null)
16444
+ return failure;
16445
+ }
16446
+ const status = yield* runProcess("claude", ["plugin", "list", "--json"], context.cwd, null);
16447
+ const statusFailure = commandFailure(status, "Could not verify Claude Code plugin installation");
16448
+ if (statusFailure !== null)
16449
+ return statusFailure;
16450
+ if (!(status.ok && isInstalledAndEnabled(status.stdout))) {
16451
+ return {
16452
+ exitCode: 1,
16453
+ output: "✗ Claude Code plugin installation finished but the plugin is not installed and enabled"
16454
+ };
16455
+ }
16456
+ return {
16457
+ exitCode: 0,
16458
+ output: `✓ Claude Code plugin installed and enabled
16459
+ Start a new Claude Code session.`
16460
+ };
16461
+ });
16462
+ function isInstalledAndEnabled(output) {
16463
+ try {
16464
+ const plugins = JSON.parse(output);
16465
+ 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);
16466
+ } catch {
16467
+ return false;
16492
16468
  }
16493
- throw new Error("bundled Code Puppy adapter is missing");
16469
+ }
16470
+ function commandFailure(result, fallback) {
16471
+ if (!result.ok)
16472
+ return { exitCode: 1, output: `✗ ${fallback}: ${result.error}` };
16473
+ return result.exitCode === 0 ? null : { exitCode: result.exitCode, output: result.output || fallback };
16494
16474
  }
16495
16475
 
16496
16476
  // 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";
16477
+ import { mkdir as mkdir2, writeFile as writeFile3 } from "node:fs/promises";
16478
+ import { join as join2 } from "node:path";
16499
16479
  var MARKETPLACE_MARKER = `Operator Memory managed Codex marketplace.
16500
16480
  `;
16501
16481
  var MARKETPLACE_NAME = "operator-memory";
16502
16482
  var PLUGIN_NAME = "aerovato";
16503
- var PLUGIN_ID = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
16483
+ var PLUGIN_ID2 = `${PLUGIN_NAME}@${MARKETPLACE_NAME}`;
16504
16484
  var MARKETPLACE = `${JSON.stringify({
16505
16485
  name: MARKETPLACE_NAME,
16506
16486
  plugins: [
@@ -16524,20 +16504,20 @@ var installCodex = fn2("installCodex")(function* (context) {
16524
16504
  if (!addMarketplace.ok && isMissingBinary(addMarketplace.error)) {
16525
16505
  return yield* registerDesktopMarketplace(context, marketplace.path);
16526
16506
  }
16527
- const addFailure = commandFailure(addMarketplace, "Codex marketplace installation failed");
16507
+ const addFailure = commandFailure2(addMarketplace, "Codex marketplace installation failed");
16528
16508
  if (addFailure !== null)
16529
16509
  return addFailure;
16530
- const install = yield* runProcess("codex", ["plugin", "add", PLUGIN_ID, "--json"], context.cwd, {
16510
+ const install = yield* runProcess("codex", ["plugin", "add", PLUGIN_ID2, "--json"], context.cwd, {
16531
16511
  NPM_CONFIG_MIN_RELEASE_AGE: "0"
16532
16512
  });
16533
- const installFailure = commandFailure(install, "Codex plugin installation failed");
16513
+ const installFailure = commandFailure2(install, "Codex plugin installation failed");
16534
16514
  if (installFailure !== null)
16535
16515
  return installFailure;
16536
16516
  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");
16517
+ const statusFailure = commandFailure2(status, "Could not verify Codex plugin installation");
16538
16518
  if (statusFailure !== null)
16539
16519
  return statusFailure;
16540
- if (!(status.ok && isInstalledAndEnabled(status.stdout))) {
16520
+ if (!(status.ok && isInstalledAndEnabled2(status.stdout))) {
16541
16521
  return {
16542
16522
  exitCode: 1,
16543
16523
  output: "✗ Codex plugin installation finished but the plugin is not installed and enabled"
@@ -16554,9 +16534,9 @@ var installCodex = fn2("installCodex")(function* (context) {
16554
16534
  });
16555
16535
  function writeMarketplace(home) {
16556
16536
  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");
16537
+ const root = join2(home, ".operator-helper", "codex-marketplace");
16538
+ const marker = join2(root, ".operator-managed");
16539
+ const marketplace = join2(root, ".agents", "plugins", "marketplace.json");
16560
16540
  try {
16561
16541
  const kind = await pathKind(root);
16562
16542
  if (kind !== "missing" && (kind !== "directory" || await readOptionalFile(marker) !== MARKETPLACE_MARKER)) {
@@ -16568,9 +16548,9 @@ function writeMarketplace(home) {
16568
16548
  }
16569
16549
  };
16570
16550
  }
16571
- await mkdir3(join3(root, ".agents", "plugins"), { recursive: true });
16572
- await writeFile4(marker, MARKETPLACE_MARKER);
16573
- await writeFile4(marketplace, MARKETPLACE);
16551
+ await mkdir2(join2(root, ".agents", "plugins"), { recursive: true });
16552
+ await writeFile3(marker, MARKETPLACE_MARKER);
16553
+ await writeFile3(marketplace, MARKETPLACE);
16574
16554
  return { ok: true, path: root };
16575
16555
  } catch (error) {
16576
16556
  return {
@@ -16588,12 +16568,12 @@ function isMissingBinary(error) {
16588
16568
  }
16589
16569
  function registerDesktopMarketplace(context, marketplacePath) {
16590
16570
  return promise2(async () => {
16591
- const codexHome = process.env.CODEX_HOME || join3(context.home, ".codex");
16592
- const config = join3(codexHome, "config.toml");
16571
+ const codexHome = process.env.CODEX_HOME || join2(context.home, ".codex");
16572
+ const config = join2(codexHome, "config.toml");
16593
16573
  try {
16594
16574
  const content = await readOptionalFile(config);
16595
- await mkdir3(codexHome, { recursive: true });
16596
- await writeFile4(config, upsertMarketplaceTable(content, marketplacePath));
16575
+ await mkdir2(codexHome, { recursive: true });
16576
+ await writeFile3(config, upsertMarketplaceTable(content, marketplacePath));
16597
16577
  } catch (error) {
16598
16578
  return {
16599
16579
  exitCode: 1,
@@ -16645,18 +16625,18 @@ ${block}`;
16645
16625
  `), ...lines.slice(end)].join(`
16646
16626
  `);
16647
16627
  }
16648
- function isInstalledAndEnabled(output) {
16628
+ function isInstalledAndEnabled2(output) {
16649
16629
  try {
16650
16630
  const value = JSON.parse(output);
16651
16631
  if (typeof value !== "object" || value === null || !("installed" in value))
16652
16632
  return false;
16653
16633
  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);
16634
+ 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
16635
  } catch {
16656
16636
  return false;
16657
16637
  }
16658
16638
  }
16659
- function commandFailure(result, fallback) {
16639
+ function commandFailure2(result, fallback) {
16660
16640
  if (!result.ok)
16661
16641
  return { exitCode: 1, output: `✗ Could not run codex: ${result.error}` };
16662
16642
  return result.exitCode === 0 ? null : { exitCode: result.exitCode, output: result.output || fallback };
@@ -16750,7 +16730,7 @@ var installPi = fn2("installPi")(function* (context) {
16750
16730
  import { resolve as resolve2 } from "node:path";
16751
16731
 
16752
16732
  // ../core/src/memory/project.ts
16753
- import { extname, join as join4, relative, sep } from "node:path";
16733
+ import { extname, join as join3, relative, sep } from "node:path";
16754
16734
 
16755
16735
  // ../core/src/utils.ts
16756
16736
  function success(value) {
@@ -16775,17 +16755,17 @@ function readString(value, key) {
16775
16755
 
16776
16756
  // ../core/src/memory/common.ts
16777
16757
  import { constants as constants2 } from "node:fs";
16778
- import { access as access3, readdir, readFile as readFile5, stat as stat3 } from "node:fs/promises";
16758
+ import { access as access3, readdir, readFile as readFile4, stat as stat3 } from "node:fs/promises";
16779
16759
  async function readOptionalContent(path) {
16780
16760
  try {
16781
- return success(await readFile5(path, "utf8"));
16761
+ return success(await readFile4(path, "utf8"));
16782
16762
  } catch (cause) {
16783
16763
  return hasNodeErrorCode(cause, "ENOENT") ? success(null) : failure({ path, cause });
16784
16764
  }
16785
16765
  }
16786
16766
  async function readContent(path) {
16787
16767
  try {
16788
- return success(await readFile5(path, "utf8"));
16768
+ return success(await readFile4(path, "utf8"));
16789
16769
  } catch (cause) {
16790
16770
  return failure({ path, cause });
16791
16771
  }
@@ -16864,13 +16844,13 @@ async function loadProjectPartition(root, loadFiles) {
16864
16844
  return hasNodeErrorCode(directory.error.cause, "ENOENT") ? success(loadFiles ? { exists: false, operatorInstructions: null, catalog: null, indexes: [] } : { exists: false }) : directory;
16865
16845
  }
16866
16846
  if (!loadFiles) {
16867
- for (const path of [join4(root, "operator.md"), join4(root, "catalog.md")]) {
16847
+ for (const path of [join3(root, "operator.md"), join3(root, "catalog.md")]) {
16868
16848
  const file = await checkOptionalReadableFile(path);
16869
16849
  if (!file.ok) {
16870
16850
  return file;
16871
16851
  }
16872
16852
  }
16873
- const indexRoot = join4(root, "index");
16853
+ const indexRoot = join3(root, "index");
16874
16854
  const indexPaths = await listMarkdownFiles(indexRoot, indexRoot);
16875
16855
  if (!indexPaths.ok) {
16876
16856
  return indexPaths;
@@ -16883,15 +16863,15 @@ async function loadProjectPartition(root, loadFiles) {
16883
16863
  }
16884
16864
  return success({ exists: true });
16885
16865
  }
16886
- const operatorInstructions = await readOptionalContent(join4(root, "operator.md"));
16866
+ const operatorInstructions = await readOptionalContent(join3(root, "operator.md"));
16887
16867
  if (!operatorInstructions.ok) {
16888
16868
  return operatorInstructions;
16889
16869
  }
16890
- const catalog = await readOptionalContent(join4(root, "catalog.md"));
16870
+ const catalog = await readOptionalContent(join3(root, "catalog.md"));
16891
16871
  if (!catalog.ok) {
16892
16872
  return catalog;
16893
16873
  }
16894
- const indexes = await readIndexes(join4(root, "index"));
16874
+ const indexes = await readIndexes(join3(root, "index"));
16895
16875
  if (!indexes.ok) {
16896
16876
  return indexes;
16897
16877
  }
@@ -16929,7 +16909,7 @@ async function listMarkdownFiles(root, directory) {
16929
16909
  }
16930
16910
  const paths = [];
16931
16911
  for (const entry of entries.value) {
16932
- const path = join4(directory, entry.name);
16912
+ const path = join3(directory, entry.name);
16933
16913
  if (entry.isDirectory()) {
16934
16914
  const nestedPaths = await listMarkdownFiles(root, path);
16935
16915
  if (!nestedPaths.ok) {
@@ -16950,14 +16930,14 @@ function compareStrings2(left, right) {
16950
16930
  }
16951
16931
 
16952
16932
  // ../core/src/memory/user.ts
16953
- import { join as join5 } from "node:path";
16933
+ import { join as join4 } from "node:path";
16954
16934
  async function loadUserPartition(root, loadFiles) {
16955
16935
  if (!loadFiles) {
16956
16936
  const directory = await readDirectory2(root);
16957
16937
  if (!directory.ok) {
16958
16938
  return hasNodeErrorCode(directory.error.cause, "ENOENT") ? success({ exists: false }) : directory;
16959
16939
  }
16960
- for (const path of [join5(root, "operator.md"), join5(root, "catalog.md")]) {
16940
+ for (const path of [join4(root, "operator.md"), join4(root, "catalog.md")]) {
16961
16941
  const file = await checkOptionalReadableFile(path);
16962
16942
  if (!file.ok) {
16963
16943
  return file;
@@ -16965,11 +16945,11 @@ async function loadUserPartition(root, loadFiles) {
16965
16945
  }
16966
16946
  return success({ exists: true });
16967
16947
  }
16968
- const operatorInstructions = await readOptionalContent(join5(root, "operator.md"));
16948
+ const operatorInstructions = await readOptionalContent(join4(root, "operator.md"));
16969
16949
  if (!operatorInstructions.ok) {
16970
16950
  return operatorInstructions;
16971
16951
  }
16972
- const catalog = await readOptionalContent(join5(root, "catalog.md"));
16952
+ const catalog = await readOptionalContent(join4(root, "catalog.md"));
16973
16953
  if (!catalog.ok) {
16974
16954
  return catalog;
16975
16955
  }
@@ -17647,103 +17627,6 @@ var describeFile = fn2("describeFile")(function* (cwd, root, file, label) {
17647
17627
  ];
17648
17628
  });
17649
17629
 
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
17630
  // src/update.ts
17748
17631
  import { realpathSync } from "node:fs";
17749
17632
  import { sep as sep2 } from "node:path";
@@ -17897,6 +17780,103 @@ var upgrade = fn2("upgrade")(function* (context) {
17897
17780
  }
17898
17781
  });
17899
17782
 
17783
+ // src/commands/user/common.ts
17784
+ var USER_ROOT = "~/.operator/user";
17785
+ var USER_FILES = ["operator.md", "catalog.md"];
17786
+
17787
+ // src/commands/user/init.ts
17788
+ var userInit = fn2("userInit")((context) => gen2(function* () {
17789
+ const pathService = yield* Path;
17790
+ const root = pathService.join(context.home, ".operator", "user");
17791
+ yield* ensurePrivateDirectory(pathService.join(context.home, ".operator"));
17792
+ const templates = {
17793
+ "operator.md": readTemplate("user/operator.md" /* UserOperator */),
17794
+ "catalog.md": readTemplate("user/catalog.md" /* UserCatalog */)
17795
+ };
17796
+ const rows = [];
17797
+ let failed = false;
17798
+ for (const file of USER_FILES) {
17799
+ const written = yield* writeTextIfMissing(pathService.join(root, file), templates[file]).pipe(result2);
17800
+ if (isFailure2(written)) {
17801
+ failed = true;
17802
+ rows.push(["Failed", `${USER_ROOT}/${file}: ${fileErrorMessage(written.failure)}`]);
17803
+ } else {
17804
+ rows.push([
17805
+ written.success === "created" ? "Created" : "Preserved",
17806
+ `${USER_ROOT}/${file}`
17807
+ ]);
17808
+ }
17809
+ }
17810
+ return {
17811
+ exitCode: failed ? 1 : 0,
17812
+ output: `✓ User Partition Initialized
17813
+ ${renderTable(rows)}`
17814
+ };
17815
+ }).pipe(catch_2((error) => succeed6(fileFailure(error))), map3((result) => ({
17816
+ ...result,
17817
+ output: `${result.output}
17818
+
17819
+ Follow the agent instructions below to complete User Setup.
17820
+
17821
+ ${readTemplate("user/setup.md" /* UserSetup */)}`
17822
+ }))));
17823
+
17824
+ // src/commands/user/status.ts
17825
+ var INIT_HINT = [
17826
+ "For Users: Run `/operator:user-init` in your harness to initialize.",
17827
+ "For Agents: Run `operator-helper user init` and follow the guide in its output."
17828
+ ];
17829
+ var userStatus = fn2("userStatus")(function* (context) {
17830
+ const pathService = yield* Path;
17831
+ const root = pathService.join(context.home, ".operator", "user");
17832
+ const kind = yield* inspectPath(root);
17833
+ if (kind === "missing") {
17834
+ return {
17835
+ exitCode: 0,
17836
+ output: [
17837
+ `User Partition: ${USER_ROOT}`,
17838
+ "",
17839
+ "⚠ User Partition Missing",
17840
+ renderTable([["Expected Location", `${USER_ROOT}/`]]),
17841
+ "",
17842
+ ...INIT_HINT
17843
+ ].join(`
17844
+ `)
17845
+ };
17846
+ }
17847
+ if (kind !== "directory") {
17848
+ return yield* new PathKindError({
17849
+ path: root,
17850
+ expected: "directory",
17851
+ actual: kind
17852
+ });
17853
+ }
17854
+ const rows = [];
17855
+ let missingCore = false;
17856
+ for (const [file, label] of [
17857
+ ["operator.md", "User Instructions"],
17858
+ ["catalog.md", "User Catalog"]
17859
+ ]) {
17860
+ const content = yield* readOptionalText(pathService.join(root, file));
17861
+ if (content === null) {
17862
+ missingCore = true;
17863
+ }
17864
+ rows.push([
17865
+ label,
17866
+ content === null ? `Missing (${USER_ROOT}/${file})` : `${USER_ROOT}/${file} (${Buffer.byteLength(content)} bytes)`
17867
+ ]);
17868
+ }
17869
+ const files = yield* listFiles(root);
17870
+ const freeform = files.filter((file) => file.relativePath !== "operator.md" && file.relativePath !== "catalog.md");
17871
+ rows.push(["Freeform Content", `${freeform.length} files`]);
17872
+ const lines = [`User Partition: ${USER_ROOT}`, "", "✓ User Partition Found", renderTable(rows)];
17873
+ if (missingCore) {
17874
+ lines.push("", ...INIT_HINT);
17875
+ }
17876
+ return { exitCode: 0, output: lines.join(`
17877
+ `) };
17878
+ });
17879
+
17900
17880
  // src/commands/version.ts
17901
17881
  var version = fn2("version")(function* (context) {
17902
17882
  const latest = yield* checkForUpdate(context.version).pipe(catch_2((error) => succeed6({ status: "failed", message: error.message })));
@@ -17928,7 +17908,7 @@ ${renderTable([
17928
17908
  ["install opencode-v2", "Install or update the OpenCode V2 plugin"],
17929
17909
  ["install deepseek", "Install or update the DeepSeek Harness plugin"],
17930
17910
  ["install pi", "Install or update the Pi plugin"],
17931
- ["install code-puppy", "Install or update the Code Puppy plugin"]
17911
+ ["install claude-code", "Install or update the Claude Code plugin"]
17932
17912
  ])}
17933
17913
 
17934
17914
  AGENT COMMANDS
@@ -17979,6 +17959,8 @@ function runCli(arguments_, context) {
17979
17959
  return yield* memoryCheck(context);
17980
17960
  case "install codex":
17981
17961
  return yield* installCodex(context);
17962
+ case "install claude-code":
17963
+ return yield* installClaudeCode(context);
17982
17964
  case "install opencode":
17983
17965
  return yield* installOpenCode(context);
17984
17966
  case "install opencode-v2":
@@ -17987,8 +17969,6 @@ function runCli(arguments_, context) {
17987
17969
  return yield* installDeepSeek(context);
17988
17970
  case "install pi":
17989
17971
  return yield* installPi(context);
17990
- case "install code-puppy":
17991
- return yield* installCodePuppy(context);
17992
17972
  case "user guide":
17993
17973
  case "project guide":
17994
17974
  case "index guide":
@@ -18027,4 +18007,4 @@ var main = gen2(function* () {
18027
18007
  }).pipe(provide4(LiveLayers));
18028
18008
  runMain2(main);
18029
18009
 
18030
- //# debugId=EB111CCB847C680664756E2164756E21
18010
+ //# debugId=BD7F91EF8CB96E6A64756E2164756E21
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.1",
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()