@timurproko/a1 0.1.8-dev.99e912e → 0.1.8-dev.9a04818

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
@@ -30,16 +30,20 @@ Pi extension packages install into A1's own profile, so bare `a1` loads them and
30
30
  `a1 pi` and `a1 sandbox` do not. Sources are Pi's: `npm:`, git, or a local path.
31
31
 
32
32
  ```sh
33
- a1 install npm:pi-mcp-adapter # install a package into ~/.a1/agent
34
- a1 remove npm:pi-mcp-adapter # remove it again (alias: a1 uninstall)
35
- a1 list # list packages installed for a1
36
- a1 update --extensions # update every installed package
37
- a1 update npm:pi-mcp-adapter # update one of them
38
- a1 update --models # refresh model catalogs
33
+ a1 pi install npm:pi-mcp-adapter # install a package into ~/.a1/agent
34
+ a1 pi remove npm:pi-mcp-adapter # remove it again (alias: a1 pi uninstall)
35
+ a1 pi list # list packages installed for a1
36
+ a1 pi update --extensions # update every installed package
37
+ a1 pi update npm:pi-mcp-adapter # update one of them
38
+ a1 update --models # refresh A1's model catalogs
39
39
  ```
40
40
 
41
41
  A running session loads a newly installed package after a restart. Pi's own
42
- profile at `~/.pi/agent` is managed by Pi itself.
42
+ profile at `~/.pi/agent` is managed by Pi itself. Extension configuration is
43
+ isolated too: for example, MCP configuration for bare `a1` belongs under
44
+ `~/.a1/agent` (or the project), so `~/.pi/agent/mcp.json` is not read. Run
45
+ `/mcp setup` inside bare `a1` to configure it; the MCP footer status appears
46
+ when that A1 configuration contains a server.
43
47
 
44
48
  ## Develop
45
49
 
@@ -5,7 +5,7 @@
5
5
  "platform": "darwin",
6
6
  "architecture": "arm64",
7
7
  "capability": "unsupported",
8
- "builtAt": "2026-08-24T07:36:29.445Z",
8
+ "builtAt": "2026-08-25T07:10:19.265Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "7524bf568992517f50ed53196c861c5edeba0d7275633bb4735ab45bd5963a28",
@@ -5,7 +5,7 @@
5
5
  "platform": "linux",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-24T07:36:31.707Z",
8
+ "builtAt": "2026-08-25T07:10:17.791Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian",
11
11
  "sha256": "29e22fe29de2828982bc67ef418c4adcaab1490281477b7bea592ec6fe621bcd",
@@ -5,10 +5,10 @@
5
5
  "platform": "win32",
6
6
  "architecture": "x64",
7
7
  "capability": "supported",
8
- "builtAt": "2026-08-24T07:36:58.529Z",
8
+ "builtAt": "2026-08-25T07:11:29.829Z",
9
9
  "artifact": {
10
10
  "filename": "process-guardian.exe",
11
- "sha256": "c8491147088aa02f51443ecef1aa9f9e36914f9c899e221668bb3a36d9140941",
11
+ "sha256": "2c1bb33c431cec4d90f29ff5fdf6575a62e19f868a8d1dd4bc09f097d60d5550",
12
12
  "size": 172544
13
13
  },
14
14
  "provenance": {
@@ -5,12 +5,13 @@ export function cliUsage(capabilities) {
5
5
  "",
6
6
  ...(capabilities.developmentProfiles ? ["pi", "sandbox"] : []),
7
7
  "version",
8
- "update [self|<source>|--extensions|--models]",
8
+ "update [self|--models]",
9
9
  "update:next",
10
10
  "update:<commit>",
11
- "install <source>",
12
- "remove <source>",
13
- "list",
11
+ "pi install <source>",
12
+ "pi remove <source>",
13
+ "pi list",
14
+ "pi update [--extensions|<source>]",
14
15
  ]);
15
16
  }
16
17
  const PROFILE_WORDS = new Set(["pi", "sandbox"]);
@@ -32,7 +33,13 @@ export function parseCliCommand(arguments_, capabilities) {
32
33
  if (arguments_.length === 0)
33
34
  return { kind: "launch", profileId: "a1" };
34
35
  const [command, ...rest] = arguments_;
35
- if (capabilities.developmentProfiles && (command === "pi" || command === "sandbox")) {
36
+ if (command === "pi") {
37
+ if (rest.length > 0)
38
+ return parsePiPackageCommand(rest);
39
+ if (capabilities.developmentProfiles)
40
+ return { kind: "launch", profileId: "pi" };
41
+ }
42
+ if (capabilities.developmentProfiles && command === "sandbox") {
36
43
  return withoutArguments(rest, { kind: "launch", profileId: command });
37
44
  }
38
45
  if (command === "version")
@@ -41,11 +48,9 @@ export function parseCliCommand(arguments_, capabilities) {
41
48
  return parseColonUpdate(command.slice("update:".length), rest);
42
49
  if (command === "update")
43
50
  return parseUpdate(rest);
44
- if (command === "install" || command === "remove" || command === "uninstall") {
45
- return parseSourceCommand(command === "install" ? "install" : "remove", rest);
51
+ if (command === "install" || command === "remove" || command === "uninstall" || command === "list") {
52
+ return packageNamespaceRejection(command, rest.join(" ") || undefined);
46
53
  }
47
- if (command === "list")
48
- return withoutArguments(rest, { kind: "packages", request: { verb: "list", source: null } });
49
54
  if (command === "ui")
50
55
  return { kind: "error", message: `The ui subcommand was removed; run bare ${PRODUCT_TEXT.commandName} for the owned UI.` };
51
56
  if (command === "agent")
@@ -71,10 +76,9 @@ function parseColonUpdate(suffix, rest) {
71
76
  return { kind: "update", channel: "next", target: suffix };
72
77
  }
73
78
  /**
74
- * `update` carries both meanings pinned Pi gives it: itself by default, and the
75
- * profile's packages when a target says so. Pi is refused as a target because A1
76
- * certifies each release against one pinned Pi, so moving Pi underneath it would
77
- * invalidate what was certified.
79
+ * Top-level `update` owns A1 itself and A1's model catalogs. Extension package
80
+ * maintenance lives under the `pi` compatibility namespace, while updating the
81
+ * pinned Pi runtime remains impossible.
78
82
  */
79
83
  function parseUpdate(rest) {
80
84
  if (rest.length === 0)
@@ -90,18 +94,42 @@ function parseUpdate(rest) {
90
94
  message: PRODUCT_TEXT.diagnostic(`pins the Pi version it was certified against; run ${PRODUCT_TEXT.commandName} update to move ${PRODUCT_TEXT.displayName} itself.`),
91
95
  };
92
96
  }
93
- // A release channel is spelled with the colon. Taking the bare word as a package
94
- // source would turn a near miss into a confident search for a package nobody has.
95
97
  if (target === "next" || target === "stable") {
96
98
  const form = target === "next" ? `${PRODUCT_TEXT.commandName} update:next` : `${PRODUCT_TEXT.commandName} update`;
97
99
  return { kind: "error", message: PRODUCT_TEXT.diagnostic(`selects a release channel with a colon; run ${form}.`) };
98
100
  }
99
- if (target === "--extensions")
100
- return { kind: "packages", request: { verb: "update", source: null } };
101
101
  if (target === "--models")
102
102
  return { kind: "packages", request: { verb: "refresh-models", source: null } };
103
+ if (target === "--extensions" || (target !== undefined && !target.startsWith("-"))) {
104
+ return packageNamespaceRejection("update", target === "--extensions" ? "--extensions" : target);
105
+ }
106
+ return unknownOption(target ?? "", "update");
107
+ }
108
+ function parsePiPackageCommand(arguments_) {
109
+ const [verb, ...rest] = arguments_;
110
+ if (verb === "install" || verb === "remove" || verb === "uninstall") {
111
+ return parseSourceCommand(verb === "install" ? "install" : "remove", rest);
112
+ }
113
+ if (verb === "list")
114
+ return withoutArguments(rest, { kind: "packages", request: { verb: "list", source: null } });
115
+ if (verb === "update")
116
+ return parsePiPackageUpdate(rest);
117
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic(`received an unknown pi package command: ${verb ?? ""}`) };
118
+ }
119
+ function parsePiPackageUpdate(rest) {
120
+ if (rest.length === 0) {
121
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic(`pi update needs --extensions or a package source.`) };
122
+ }
123
+ if (rest.length > 1)
124
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic("pi update accepts one target.") };
125
+ const [target] = rest;
126
+ if (target === "--extensions")
127
+ return { kind: "packages", request: { verb: "update", source: null } };
128
+ if (target === "--models") {
129
+ return { kind: "error", message: PRODUCT_TEXT.diagnostic(`refreshes its model catalogs at the top level; run ${PRODUCT_TEXT.commandName} update --models.`) };
130
+ }
103
131
  if (target === undefined || target.startsWith("-"))
104
- return unknownOption(target ?? "", "update");
132
+ return unknownOption(target ?? "", "pi update");
105
133
  if (PROFILE_WORDS.has(target))
106
134
  return profileRejection("update");
107
135
  return { kind: "packages", request: { verb: "update", source: target } };
@@ -129,6 +157,13 @@ function withoutArguments(rest, command) {
129
157
  return profileRejection("list");
130
158
  return { kind: "error", message: PRODUCT_TEXT.diagnostic("commands do not accept additional arguments.") };
131
159
  }
160
+ function packageNamespaceRejection(verb, target) {
161
+ const suffix = target === undefined ? "" : ` ${target}`;
162
+ return {
163
+ kind: "error",
164
+ message: PRODUCT_TEXT.diagnostic(`manages extension packages under its pi namespace; run ${PRODUCT_TEXT.commandName} pi ${verb}${suffix}.`),
165
+ };
166
+ }
132
167
  function profileRejection(verb) {
133
168
  return {
134
169
  kind: "error",
@@ -1,6 +1,7 @@
1
1
  import { AssistantMessageComponent, BashExecutionComponent, CompactionSummaryMessageComponent, CustomMessageComponent, DynamicBorder, getMarkdownTheme, parseSkillBlock, ToolExecutionComponent, UserMessageComponent, } from "@earendil-works/pi-coding-agent";
2
2
  import { SkillInvocationMessageComponent, } from "./upstream/components/skill-invocation-message.js";
3
3
  import { Container, Markdown, Spacer, Text, } from "#pi-tui";
4
+ import { PRODUCT_TEXT } from "../../product-identity.js";
4
5
  import { KeybindingsManager, } from "./upstream/adjacent/core/keybindings.js";
5
6
  import { PINNED_PI_LAYOUT, piTheme, } from "./theme.js";
6
7
  import { componentPort, createTuiFacade, ensureTheme, formatSessionTokens, isRecord, } from "./shell-shared-facade.js";
@@ -168,7 +169,7 @@ export function renderPiShellPackageUpdateNotice(packages, width) {
168
169
  container.addChild(new Spacer(1));
169
170
  container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
170
171
  container.addChild(new Text(`${theme.bold(theme.fg("warning", "Package Updates Available"))}\n`
171
- + `${theme.fg("muted", "Package updates are available. Run ")}${theme.fg("accent", "pi update --extensions")}\n`
172
+ + `${theme.fg("muted", "Package updates are available. Run ")}${theme.fg("accent", `${PRODUCT_TEXT.commandName} pi update --extensions`)}\n`
172
173
  + `${theme.fg("muted", "Packages:")}\n`
173
174
  + packages.map(name => `- ${name}`).join("\n"), 1, 0));
174
175
  container.addChild(new DynamicBorder(text => theme.fg("warning", text)));
@@ -11,6 +11,12 @@ import { createPiRuntimeIntegration } from "./runtime-integration.js";
11
11
  import { PiSessionCommandIntegration } from "./session-integration.js";
12
12
  import { PiSettingsIntegration } from "./settings-integration.js";
13
13
  const execFileAsync = promisify(execFile);
14
+ /**
15
+ * Engine events delivered before the queue hands the event loop a turn. Small enough
16
+ * that a streaming burst never holds input, large enough that an ordinary turn is one
17
+ * batch.
18
+ */
19
+ const EVENT_DELIVERY_BATCH = 16;
14
20
  const DEFAULT_SURFACE = {
15
21
  columns: 100,
16
22
  rows: 32,
@@ -61,6 +67,8 @@ export class PiEngineAdapter {
61
67
  #eventQueue = [];
62
68
  #eventQueueProcessing;
63
69
  #droppedEventCount = 0;
70
+ #agentRunActive = false;
71
+ #statusKind = null;
64
72
  #sessionCommands;
65
73
  #gitBranch = null;
66
74
  #extensionUi;
@@ -143,7 +151,7 @@ export class PiEngineAdapter {
143
151
  if (this.#disposed || updates.length === 0)
144
152
  return;
145
153
  const packages = updates.map(name => `- ${name}`).join("\n");
146
- this.#addDiagnostic("info", "package-updates", `Package updates are available. Run pi update --extensions\nPackages:\n${packages}`, true);
154
+ this.#addDiagnostic("info", "package-updates", `Package updates are available. Run ${PRODUCT_IDENTITY.commandName} pi update --extensions\nPackages:\n${packages}`, true);
147
155
  this.#emitView();
148
156
  }
149
157
  onEvent(listener) {
@@ -373,9 +381,10 @@ export class PiEngineAdapter {
373
381
  }
374
382
  const extensionCommands = this.#session?.extensionRunner?.getRegisteredCommands?.();
375
383
  if (Array.isArray(extensionCommands)) {
376
- for (const command of extensionCommands.filter(isRecord)) {
377
- const name = stringProperty(command, "name");
378
- if (!name || usedNames.has(name))
384
+ const registered = extensionCommands.filter(isRecord);
385
+ for (const command of registered) {
386
+ const name = stringProperty(command, "invocationName") ?? stringProperty(command, "name");
387
+ if (!name || usedNames.has(name) || isPiPrefixedCompatibilityAlias(command, registered))
379
388
  continue;
380
389
  commands.push({ name, description: stringProperty(command, "description") ?? "Extension command", source: "extension" });
381
390
  usedNames.add(name);
@@ -1350,6 +1359,8 @@ export class PiEngineAdapter {
1350
1359
  submitEnabled: true,
1351
1360
  };
1352
1361
  this.#status = { ...this.#status, workingMessage: null, badges: [] };
1362
+ this.#statusKind = null;
1363
+ this.#agentRunActive = false;
1353
1364
  this.#activeModel = readModel(session.model);
1354
1365
  this.#reconcileActiveModelAvailability();
1355
1366
  this.#thinkingLevel = readThinkingLevel(session.thinkingLevel);
@@ -1402,15 +1413,45 @@ export class PiEngineAdapter {
1402
1413
  this.#emitView();
1403
1414
  }
1404
1415
  }
1416
+ /** Shows the state named by `kind`, which becomes the state a later end can clear. */
1417
+ #enterWorkState(kind, message) {
1418
+ const wasBusy = this.#lifecycle === "busy";
1419
+ this.#statusKind = kind;
1420
+ this.#lifecycle = "busy";
1421
+ this.#status = { ...this.#status, workingMessage: message };
1422
+ if (!wasBusy)
1423
+ this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
1424
+ this.#emitEvent({ type: "status", status: this.#status });
1425
+ }
1426
+ /**
1427
+ * Ends one named state. A state the shell is not in is left alone, so a finished
1428
+ * compaction or retry cannot clear the working state it never replaced. While the run
1429
+ * continues, ending either of those returns to working rather than to idle.
1430
+ */
1431
+ #endWorkState(kind) {
1432
+ if (this.#statusKind !== kind)
1433
+ return;
1434
+ if (this.#agentRunActive) {
1435
+ this.#enterWorkState("working", "Working...");
1436
+ return;
1437
+ }
1438
+ this.#leaveWorkStates();
1439
+ }
1440
+ /** Leaves every work state and reports the session idle. */
1441
+ #leaveWorkStates() {
1442
+ this.#statusKind = null;
1443
+ this.#lifecycle = "ready";
1444
+ this.#status = { ...this.#status, workingMessage: null };
1445
+ this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
1446
+ this.#emitEvent({ type: "status", status: this.#status });
1447
+ }
1405
1448
  #handlePiEvent(event) {
1406
1449
  if (!isRecord(event) || typeof event.type !== "string")
1407
1450
  return;
1408
1451
  switch (event.type) {
1409
1452
  case "agent_start":
1410
- this.#lifecycle = "busy";
1411
- this.#status = { ...this.#status, workingMessage: "Working..." };
1412
- this.#emitEvent({ type: "session-lifecycle", lifecycle: "busy", reason: null });
1413
- this.#emitEvent({ type: "status", status: this.#status });
1453
+ this.#agentRunActive = true;
1454
+ this.#enterWorkState("working", "Working...");
1414
1455
  return;
1415
1456
  case "message_start":
1416
1457
  this.#upsertMessageBlock(event.message, "live");
@@ -1453,10 +1494,17 @@ export class PiEngineAdapter {
1453
1494
  this.#rebuildTranscript(finalMessages, "finalized");
1454
1495
  else
1455
1496
  this.#transcript = this.#transcript.map(block => block.status === "live" ? { ...block, status: "finalized" } : block);
1456
- this.#lifecycle = "ready";
1457
- this.#status = { ...this.#status, workingMessage: null };
1458
- this.#emitEvent({ type: "session-lifecycle", lifecycle: "ready", reason: null });
1459
- this.#emitEvent({ type: "status", status: this.#status });
1497
+ // Ending a turn leaves the working state, as the recorded pinned baseline does, but
1498
+ // it leaves only that state: a compaction or retry being shown outlives the turn
1499
+ // that ended under it. Settlement ends the run, and with it every state — the
1500
+ // engine ends a turn for each continuation it makes and settles once.
1501
+ if (event.type === "agent_settled") {
1502
+ this.#agentRunActive = false;
1503
+ this.#leaveWorkStates();
1504
+ }
1505
+ else if (this.#statusKind === null || this.#statusKind === "working") {
1506
+ this.#leaveWorkStates();
1507
+ }
1460
1508
  this.#emitView();
1461
1509
  return;
1462
1510
  }
@@ -1472,20 +1520,16 @@ export class PiEngineAdapter {
1472
1520
  return;
1473
1521
  }
1474
1522
  case "auto_retry_start":
1475
- this.#lifecycle = "busy";
1476
- this.#status = { ...this.#status, workingMessage: "Retrying…" };
1477
- this.#emitEvent({ type: "status", status: this.#status });
1523
+ this.#enterWorkState("retry", "Retrying…");
1478
1524
  return;
1479
1525
  case "auto_retry_end":
1480
- case "compaction_end":
1481
- this.#lifecycle = "ready";
1482
- this.#status = { ...this.#status, workingMessage: null };
1483
- this.#emitEvent({ type: "status", status: this.#status });
1526
+ this.#endWorkState("retry");
1484
1527
  return;
1485
1528
  case "compaction_start":
1486
- this.#lifecycle = "busy";
1487
- this.#status = { ...this.#status, workingMessage: "Compacting…" };
1488
- this.#emitEvent({ type: "status", status: this.#status });
1529
+ this.#enterWorkState("compaction", "Compacting…");
1530
+ return;
1531
+ case "compaction_end":
1532
+ this.#endWorkState("compaction");
1489
1533
  return;
1490
1534
  case "thinking_level_changed":
1491
1535
  this.#thinkingLevel = readThinkingLevel(event.level);
@@ -1847,6 +1891,7 @@ export class PiEngineAdapter {
1847
1891
  }
1848
1892
  async #processEventQueue() {
1849
1893
  try {
1894
+ let deliveredSinceYield = 0;
1850
1895
  while (this.#eventQueue.length > 0) {
1851
1896
  const event = this.#eventQueue.shift();
1852
1897
  if (!event)
@@ -1859,6 +1904,14 @@ export class PiEngineAdapter {
1859
1904
  this.#recordDiagnostic("warning", "event-listener", error instanceof Error ? error.message : String(error), true);
1860
1905
  }
1861
1906
  }
1907
+ deliveredSinceYield += 1;
1908
+ // A microtask chain runs to exhaustion before the loop turns, so a streaming
1909
+ // burst would hold typed input, pointer reports, and timed indicators until it
1910
+ // drained. Yielding on a macrotask hands those their turn between batches.
1911
+ if (deliveredSinceYield >= EVENT_DELIVERY_BATCH && this.#eventQueue.length > 0) {
1912
+ deliveredSinceYield = 0;
1913
+ await new Promise(resolve => { setImmediate(resolve); });
1914
+ }
1862
1915
  }
1863
1916
  }
1864
1917
  finally {
@@ -2274,6 +2327,26 @@ function stringProperty(value, key) {
2274
2327
  const item = value[key];
2275
2328
  return typeof item === "string" && item.length > 0 ? item : undefined;
2276
2329
  }
2330
+ /**
2331
+ * Some ecosystem extensions retain a `pi-<name>` slash-command alias beside
2332
+ * their unprefixed command. A1 presents the product-neutral command once while
2333
+ * leaving Pi's runner free to accept the compatibility alias when typed.
2334
+ */
2335
+ function isPiPrefixedCompatibilityAlias(command, commands) {
2336
+ const name = stringProperty(command, "name");
2337
+ if (!name?.startsWith("pi-") || name.length === 3)
2338
+ return false;
2339
+ const canonicalName = name.slice(3);
2340
+ const description = stringProperty(command, "description");
2341
+ const sourcePath = extensionCommandSourcePath(command);
2342
+ return commands.some(candidate => candidate !== command
2343
+ && stringProperty(candidate, "name") === canonicalName
2344
+ && stringProperty(candidate, "description") === description
2345
+ && extensionCommandSourcePath(candidate) === sourcePath);
2346
+ }
2347
+ function extensionCommandSourcePath(command) {
2348
+ return isRecord(command) ? stringProperty(command.sourceInfo, "path") : undefined;
2349
+ }
2277
2350
  function compactResourceLabel(path) {
2278
2351
  const segments = path.replaceAll("\\", "/").split("/").filter(Boolean);
2279
2352
  return segments.at(-1) ?? path;
@@ -638,8 +638,10 @@ export class OwnedUiSessionShell {
638
638
  });
639
639
  this.root.editor.setAutocompleteCommands(this.backend.workflowAutocompleteCommands());
640
640
  this.#unsubscribe = this.backend.onEvent(event => {
641
- this.#syncView();
642
- if (this.view().lifecycle === "ready" && this.#compactionQueue.length > 0)
641
+ // One view read per event: the model is built by the backend, and building it
642
+ // twice per streamed chunk is what made a long session cost more per chunk.
643
+ const view = this.#syncView();
644
+ if (view.lifecycle === "ready" && this.#compactionQueue.length > 0)
643
645
  void this.#flushCompactionQueue();
644
646
  if (event.type === "session-lifecycle" && event.lifecycle === "stopped")
645
647
  this.#resolveStopped?.();
@@ -1257,6 +1259,7 @@ export class OwnedUiSessionShell {
1257
1259
  this.runtime.requestRender();
1258
1260
  for (const listener of this.#listeners)
1259
1261
  listener(view);
1262
+ return view;
1260
1263
  }
1261
1264
  #openOwnedRoute(route) {
1262
1265
  const surface = this.#routeHost?.open(route) ?? null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurproko/a1",
3
- "version": "0.1.8-dev.99e912e",
3
+ "version": "0.1.8-dev.9a04818",
4
4
  "description": "Standalone terminal workspace for supervised native and managed agents",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.13.0",