@phux/opencode 0.2.1 → 0.3.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
@@ -1,6 +1,6 @@
1
1
  # @phux/opencode
2
2
 
3
- OpenCode V2 plugin for operating shared terminals through an external local
3
+ OpenCode plugin for operating shared terminals through an external local
4
4
  phux server and appending cache-preserving fleet context. Installation,
5
5
  configuration, tool behavior, target precedence,
6
6
  lifecycle gaps, and safety boundaries live in the canonical
@@ -17,7 +17,7 @@ npm run gates
17
17
  npm run smoke:opencode
18
18
  ```
19
19
 
20
- `smoke:opencode` starts an isolated OpenCode V2 server and verifies that its
20
+ `smoke:opencode` starts an isolated OpenCode server and verifies that its
21
21
  resolved config accepts the built plugin URL. Plugin setup, registration,
22
22
  cleanup, and tool behavior are covered by `npm run gates`. The smoke requires
23
23
  an `opencode2` executable; set `OPENCODE_BIN` to choose it.
package/dist/index.d.ts CHANGED
@@ -224,6 +224,30 @@ interface AgentRecord {
224
224
  readonly attention?: AgentAttention;
225
225
  readonly session: string;
226
226
  }
227
+ /** Closed `AgentEventsJsonlV1` record types (`docs/spec/L1.md`). */
228
+ declare const AGENT_EVENT_TYPES: readonly ["session_start", "prompt", "tool_start", "tool_end", "notification", "ask", "stop", "session_end", "state", "provider_raw"];
229
+ type AgentEventType = (typeof AGENT_EVENT_TYPES)[number];
230
+ /** `phux agent session open --json` document. */
231
+ interface AgentSessionOpenResult {
232
+ readonly schema_version: 1;
233
+ readonly resource: string;
234
+ readonly parent: string;
235
+ readonly provider: string;
236
+ readonly native_id: string | null;
237
+ }
238
+ /** `phux agent emit --json` stamped header. */
239
+ interface AgentEmitResult {
240
+ readonly schema_version: 1;
241
+ readonly resource: string;
242
+ readonly seq: number;
243
+ readonly ts_ms: number;
244
+ readonly type: AgentEventType;
245
+ }
246
+ /** Projection of `phux agent session close`'s `@N\\tclosed` line. */
247
+ interface AgentSessionCloseResult {
248
+ readonly resource: string;
249
+ readonly closed: true;
250
+ }
227
251
 
228
252
  interface PhuxCliOptions {
229
253
  readonly executable?: string;
@@ -271,6 +295,13 @@ interface RunOptions extends ExecutionOptions {
271
295
  interface AgentTargetOptions extends ExecutionOptions {
272
296
  readonly target: string;
273
297
  }
298
+ interface AgentSessionOpenOptions extends ExecutionOptions {
299
+ readonly provider: string;
300
+ readonly nativeId?: string;
301
+ }
302
+ interface AgentEmitOptions extends ExecutionOptions {
303
+ readonly data?: Readonly<Record<string, unknown>>;
304
+ }
274
305
  type SplitDirection = "horizontal" | "vertical";
275
306
  interface PlacementOptions {
276
307
  readonly target?: string;
@@ -344,6 +375,12 @@ declare class PhuxCli {
344
375
  agentSet(target: string, record: AgentRecord, options?: ExecutionOptions): Promise<AgentRecord>;
345
376
  /** Clear a declaration and require the CLI's confirmed tombstone response. */
346
377
  agentClear(target: string, options?: ExecutionOptions): Promise<void>;
378
+ /** Open an AgentSession bound to a pane; the caller becomes its producer. */
379
+ agentSessionOpen(target: string, options: AgentSessionOpenOptions): Promise<AgentSessionOpenResult>;
380
+ /** Append one closed-type record. No-op-refused by the server if this client is not the opener. */
381
+ agentEmit(target: string, type: AgentEventType, options?: AgentEmitOptions): Promise<AgentEmitResult>;
382
+ /** Close a pane's AgentSession; the parent pane is untouched. */
383
+ agentSessionClose(target: string, options?: ExecutionOptions): Promise<AgentSessionCloseResult>;
347
384
  renderedSnapshot(options: RenderedSnapshotOptions): Promise<RenderedFrame>;
348
385
  snapshot(options?: SnapshotOptions): Promise<ScreenState>;
349
386
  wait(options?: WaitOptions): Promise<WaitOutcome>;
@@ -470,6 +507,9 @@ interface OpenCodeLifecycleAdapter {
470
507
  }): Promise<AgentStateList>;
471
508
  agentSet(target: string, record: AgentRecord, options?: ExecutionOptions): Promise<AgentRecord>;
472
509
  agentClear(target: string, options?: ExecutionOptions): Promise<void>;
510
+ agentSessionOpen?(target: string, options: AgentSessionOpenOptions): Promise<AgentSessionOpenResult>;
511
+ agentEmit?(target: string, type: AgentEventType, options?: AgentEmitOptions): Promise<AgentEmitResult>;
512
+ agentSessionClose?(target: string, options?: ExecutionOptions): Promise<AgentSessionCloseResult>;
473
513
  }
474
514
  interface OpenCodeLifecycleOptions {
475
515
  readonly cli?: OpenCodeLifecycleAdapter;
@@ -488,6 +528,8 @@ declare class OpenCodeLifecycle {
488
528
  private readonly target;
489
529
  private readonly states;
490
530
  private readonly owned;
531
+ private readonly sessions;
532
+ private readonly openedPanes;
491
533
  private tail;
492
534
  private disposed;
493
535
  constructor(options: OpenCodeLifecycleOptions);
@@ -502,6 +544,9 @@ declare class OpenCodeLifecycle {
502
544
  observeState(sessionId: string, state: OpenCodeLifecycleState): Promise<void>;
503
545
  /** A tool invocation is an honest working signal if no status event was seen yet. */
504
546
  targetSelected(sessionId: string): Promise<void>;
547
+ ask(sessionId: string, data?: Readonly<Record<string, unknown>>): Promise<void>;
548
+ toolStart(sessionId: string, toolName: string, toolUseId?: string): Promise<void>;
549
+ toolEnd(sessionId: string, toolName: string, toolUseId?: string): Promise<void>;
505
550
  deleteSession(sessionId: string): Promise<void>;
506
551
  dispose(): Promise<void>;
507
552
  settled(): Promise<void>;
@@ -510,6 +555,10 @@ declare class OpenCodeLifecycle {
510
555
  private clearSession;
511
556
  private clearOwned;
512
557
  private execution;
558
+ private emitter;
559
+ private bindSession;
560
+ private emit;
561
+ private finishSession;
513
562
  }
514
563
  interface OpenCodeLifecycleEvent {
515
564
  readonly type: string;
@@ -528,8 +577,8 @@ interface PhuxOpenCodeOptions {
528
577
  readonly env?: NodeJS.ProcessEnv;
529
578
  readonly onLifecycleError?: (error: unknown) => void;
530
579
  }
531
- /** Build a V2 plugin with optional test-only defaults. */
532
- declare function createPhuxPlugin(defaults?: PhuxOpenCodeOptions): Plugin.Plugin;
533
- declare const PhuxPlugin: Plugin.Plugin;
580
+ /** Build an OpenCode plugin with optional test-only defaults. */
581
+ declare function createPhuxPlugin(defaults?: PhuxOpenCodeOptions): Plugin;
582
+ declare const PhuxPlugin: Plugin;
534
583
 
535
584
  export { type AgentTargetOptions, type CreateOptions, DEFAULT_SHORT_TIMEOUT_MS, type ExecutionOptions, MAX_MODEL_BYTES, MAX_MODEL_LINES, OpenCodeLifecycle, type OpenCodeLifecycleAdapter, type OpenCodeLifecycleEvent, type OpenCodeLifecycleOptions, type OpenCodeLifecycleState, PhuxCli, type PhuxCliOptions, PhuxContextAwareness, type PhuxContextAwarenessOptions, type PhuxContextEmission, type PhuxContextIdentity, type PhuxOpenCodeOptions, PhuxPlugin, type PhuxProbe, type PhuxToolDefinition, type PhuxToolMetadata, type PhuxToolRuntime, type RunOptions, type SnapshotOptions, type ToolContext, type ToolResult, type WaitOptions, type WaitOutcome, boundedResult, contextAwarenessEnabled, createPhuxPlugin, createPhuxTools, PhuxPlugin as default, handleLifecycleEvent, normalizeTerminalIdentity, resolveTarget };
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // src/index.ts
2
- import { Plugin } from "@opencode-ai/plugin";
2
+ import {
3
+ tool
4
+ } from "@opencode-ai/plugin";
3
5
 
4
- // ../pi/src/errors.ts
6
+ // ../runtime/src/errors.ts
5
7
  var PhuxError = class extends Error {
6
8
  code;
7
9
  argv;
@@ -17,7 +19,7 @@ var PhuxError = class extends Error {
17
19
  }
18
20
  };
19
21
 
20
- // ../pi/src/runner.ts
22
+ // ../runtime/src/runner.ts
21
23
  import { spawn } from "child_process";
22
24
  var DEFAULT_MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
23
25
  var nodeProcessRunner = (request) => new Promise((resolve, reject) => {
@@ -141,7 +143,19 @@ function isNoSuchProcess(error) {
141
143
  return error instanceof Error && "code" in error && error.code === "ESRCH";
142
144
  }
143
145
 
144
- // ../pi/src/schemas.ts
146
+ // ../runtime/src/schemas.ts
147
+ var AGENT_EVENT_TYPES = [
148
+ "session_start",
149
+ "prompt",
150
+ "tool_start",
151
+ "tool_end",
152
+ "notification",
153
+ "ask",
154
+ "stop",
155
+ "session_end",
156
+ "state",
157
+ "provider_raw"
158
+ ];
145
159
  var SchemaValidationError = class extends Error {
146
160
  constructor(path, expectation) {
147
161
  super(`${path} must be ${expectation}`);
@@ -555,8 +569,49 @@ function parseAgentStateList(value) {
555
569
  });
556
570
  return { schema_version: 1, agents };
557
571
  }
572
+ function isAgentEventType(value) {
573
+ return AGENT_EVENT_TYPES.includes(value);
574
+ }
575
+ function parseAgentSessionOpenResult(value) {
576
+ const root = record(value, "$ (phux agent session open --json CLI shape)");
577
+ if (root.schema_version !== 1) {
578
+ throw new SchemaValidationError("$.schema_version", "the supported value 1");
579
+ }
580
+ const resource = string(root.resource, "$.resource");
581
+ const parent = string(root.parent, "$.parent");
582
+ const provider = string(root.provider, "$.provider");
583
+ if (resource.trim().length === 0) throw new SchemaValidationError("$.resource", "non-empty");
584
+ if (parent.trim().length === 0) throw new SchemaValidationError("$.parent", "non-empty");
585
+ if (provider.trim().length === 0) throw new SchemaValidationError("$.provider", "non-empty");
586
+ return {
587
+ schema_version: 1,
588
+ resource,
589
+ parent,
590
+ provider,
591
+ native_id: root.native_id === null || root.native_id === void 0 ? null : string(root.native_id, "$.native_id")
592
+ };
593
+ }
594
+ function parseAgentEmitResult(value) {
595
+ const root = record(value, "$ (phux agent emit --json CLI shape)");
596
+ if (root.schema_version !== 1) {
597
+ throw new SchemaValidationError("$.schema_version", "the supported value 1");
598
+ }
599
+ const resource = string(root.resource, "$.resource");
600
+ if (resource.trim().length === 0) throw new SchemaValidationError("$.resource", "non-empty");
601
+ const type = string(root.type, "$.type");
602
+ if (!isAgentEventType(type)) {
603
+ throw new SchemaValidationError("$.type", AGENT_EVENT_TYPES.map((item) => JSON.stringify(item)).join(", "));
604
+ }
605
+ return {
606
+ schema_version: 1,
607
+ resource,
608
+ seq: integer(root.seq, "$.seq", 1),
609
+ ts_ms: integer(root.ts_ms, "$.ts_ms", 0),
610
+ type
611
+ };
612
+ }
558
613
 
559
- // ../pi/src/adapter.ts
614
+ // ../runtime/src/adapter.ts
560
615
  var MINIMUM_PHUX_VERSION = "0.1.0";
561
616
  var VERSION_PATTERN = /^phux\s+v?(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/;
562
617
  var PhuxCli = class {
@@ -723,6 +778,44 @@ var PhuxCli = class {
723
778
  throw invalidResponse("agent clear", this.executable, args, "expected @N\\t- confirmation");
724
779
  }
725
780
  }
781
+ /** Open an AgentSession bound to a pane; the caller becomes its producer. */
782
+ async agentSessionOpen(target, options) {
783
+ if (target.trim().length === 0) throw new TypeError("target must be non-empty");
784
+ if (options.provider.trim().length === 0) throw new TypeError("provider must be non-empty");
785
+ const args = ["agent", "session", "open", target, "--provider", options.provider];
786
+ if (options.nativeId !== void 0) {
787
+ if (options.nativeId.trim().length === 0) throw new TypeError("nativeId must be non-empty");
788
+ args.push("--native-id", options.nativeId);
789
+ }
790
+ args.push("--json");
791
+ this.pushSocket(args);
792
+ return this.jsonCommand("agent session open", args, options, parseAgentSessionOpenResult);
793
+ }
794
+ /** Append one closed-type record. No-op-refused by the server if this client is not the opener. */
795
+ async agentEmit(target, type, options = {}) {
796
+ if (target.trim().length === 0) throw new TypeError("target must be non-empty");
797
+ if (!isAgentEventType(type)) {
798
+ throw new TypeError(`type must be one of ${AGENT_EVENT_TYPES.join(", ")}`);
799
+ }
800
+ const args = ["agent", "emit", target, "--type", type];
801
+ if (options.data !== void 0) {
802
+ if (typeof options.data !== "object" || Array.isArray(options.data)) {
803
+ throw new TypeError("data must be a JSON object");
804
+ }
805
+ args.push("--data", JSON.stringify(options.data));
806
+ }
807
+ args.push("--json");
808
+ this.pushSocket(args);
809
+ return this.jsonCommand("agent emit", args, options, parseAgentEmitResult);
810
+ }
811
+ /** Close a pane's AgentSession; the parent pane is untouched. */
812
+ async agentSessionClose(target, options = {}) {
813
+ if (target.trim().length === 0) throw new TypeError("target must be non-empty");
814
+ const args = ["agent", "session", "close", target];
815
+ this.pushSocket(args);
816
+ const result = await this.completed("agent session close", args, options, false);
817
+ return parseSessionClosed("agent session close", this.executable, result.stdout, args);
818
+ }
726
819
  async renderedSnapshot(options) {
727
820
  requirePositiveInteger(options.cols, "cols");
728
821
  requirePositiveInteger(options.rows, "rows");
@@ -942,6 +1035,15 @@ function parseAgentConfirmation(verb, executable, stdout, args) {
942
1035
  }
943
1036
  return parseJson(verb, executable, line.slice(tab + 1), args, parseAgentRecord);
944
1037
  }
1038
+ function parseSessionClosed(verb, executable, stdout, args) {
1039
+ const line = stdout.trim();
1040
+ const tab = line.indexOf(" ");
1041
+ const resource = tab < 0 ? "" : line.slice(0, tab);
1042
+ if (resource.length === 0 || line.slice(tab + 1) !== "closed") {
1043
+ throw invalidResponse(verb, executable, args, "expected @N\\tclosed confirmation");
1044
+ }
1045
+ return { resource, closed: true };
1046
+ }
945
1047
  function parseWatchLines(stdout, executable, args) {
946
1048
  const lines = stdout.split("\n").filter((line) => line.trim().length > 0);
947
1049
  return lines.map((line, index) => {
@@ -1071,8 +1173,117 @@ function requirePositiveInteger(value, name) {
1071
1173
  throw new RangeError(`${name} must be a positive safe integer`);
1072
1174
  }
1073
1175
  }
1176
+ function hasAgentSessionCli(cli) {
1177
+ const candidate = cli;
1178
+ return typeof candidate.agentSessionOpen === "function" && typeof candidate.agentEmit === "function" && typeof candidate.agentSessionClose === "function";
1179
+ }
1180
+ function isAgentSessionUnsupported(error) {
1181
+ if (!(error instanceof PhuxError) || error.code !== "command_failed") return false;
1182
+ const haystack = `${error.message}
1183
+ ${error.stderr ?? ""}`;
1184
+ return /unsupported_server|unrecognized subcommand|invalid subcommand|unknown command|not a valid command/i.test(haystack);
1185
+ }
1186
+ var AgentSessionEmitter = class {
1187
+ cli;
1188
+ provider;
1189
+ onError;
1190
+ target = null;
1191
+ nativeId = null;
1192
+ opened = false;
1193
+ unavailable = false;
1194
+ constructor(cli, options) {
1195
+ this.cli = cli !== null && hasAgentSessionCli(cli) ? cli : null;
1196
+ this.provider = options.provider;
1197
+ this.onError = options.onError ?? (() => {
1198
+ });
1199
+ if (this.cli === null) this.unavailable = true;
1200
+ if (this.provider.trim().length === 0) throw new TypeError("provider must be non-empty");
1201
+ }
1202
+ get isOpen() {
1203
+ return this.opened;
1204
+ }
1205
+ get isUnavailable() {
1206
+ return this.unavailable;
1207
+ }
1208
+ /** Take over a session this process left open (extension reload). */
1209
+ adopt(target) {
1210
+ this.target = target;
1211
+ this.opened = target !== null;
1212
+ this.nativeId = null;
1213
+ }
1214
+ async bind(target, nativeId, options = {}) {
1215
+ if (this.unavailable) return;
1216
+ if (target === this.target && this.opened && this.nativeId === nativeId) return;
1217
+ if (this.opened && (this.target !== target || this.nativeId !== nativeId)) {
1218
+ await this.finish(options);
1219
+ }
1220
+ if (target === null) return;
1221
+ await this.open(target, nativeId, options);
1222
+ }
1223
+ async emit(type, data, options = {}) {
1224
+ if (this.unavailable || !this.opened || this.target === null || this.cli === null) return;
1225
+ try {
1226
+ await this.cli.agentEmit(this.target, type, {
1227
+ ...options,
1228
+ ...data === void 0 ? {} : { data }
1229
+ });
1230
+ } catch (error) {
1231
+ this.onError(error);
1232
+ }
1233
+ }
1234
+ async finish(options = {}) {
1235
+ if (this.unavailable || !this.opened || this.target === null || this.cli === null) {
1236
+ this.opened = false;
1237
+ this.target = null;
1238
+ this.nativeId = null;
1239
+ return;
1240
+ }
1241
+ const target = this.target;
1242
+ this.opened = false;
1243
+ this.target = null;
1244
+ this.nativeId = null;
1245
+ try {
1246
+ await this.cli.agentEmit(target, "session_end", options);
1247
+ } catch (error) {
1248
+ this.onError(error);
1249
+ }
1250
+ try {
1251
+ await this.cli.agentSessionClose(target, options);
1252
+ } catch (error) {
1253
+ this.onError(error);
1254
+ }
1255
+ }
1256
+ async open(target, nativeId, options) {
1257
+ if (this.cli === null) return;
1258
+ try {
1259
+ await this.cli.agentSessionOpen(target, {
1260
+ provider: this.provider,
1261
+ nativeId,
1262
+ ...options
1263
+ });
1264
+ } catch (error) {
1265
+ if (isAgentSessionUnsupported(error)) {
1266
+ this.unavailable = true;
1267
+ return;
1268
+ }
1269
+ this.onError(error);
1270
+ return;
1271
+ }
1272
+ this.target = target;
1273
+ this.nativeId = nativeId;
1274
+ this.opened = true;
1275
+ try {
1276
+ await this.cli.agentEmit(target, "session_start", {
1277
+ ...options,
1278
+ data: { provider: this.provider, native_id: nativeId }
1279
+ });
1280
+ } catch (error) {
1281
+ this.onError(error);
1282
+ }
1283
+ }
1284
+ };
1074
1285
 
1075
- // ../pi/src/awareness.ts
1286
+ // ../runtime/src/awareness.ts
1076
1287
  var PHUX_CONTEXT_VERSION = 1;
1077
1288
  var DEFAULT_CONTEXT_TIMEOUT_MS = 1e3;
1078
1289
  var DEFAULT_CONTEXT_MAX_BYTES = 8 * 1024;
@@ -1320,6 +1531,8 @@ var OpenCodeLifecycle = class {
1320
1531
  target;
1321
1532
  states = /* @__PURE__ */ new Map();
1322
1533
  owned = /* @__PURE__ */ new Map();
1534
+ sessions = /* @__PURE__ */ new Map();
1535
+ openedPanes = /* @__PURE__ */ new Map();
1323
1536
  tail = Promise.resolve();
1324
1537
  disposed = false;
1325
1538
  constructor(options) {
@@ -1343,15 +1556,50 @@ var OpenCodeLifecycle = class {
1343
1556
  observeState(sessionId, state) {
1344
1557
  if (this.disposed) return this.tail;
1345
1558
  this.states.set(sessionId, state);
1346
- return this.enqueue(() => this.publish(sessionId));
1559
+ return this.enqueue(async () => {
1560
+ await this.publish(sessionId);
1561
+ await this.emit(sessionId, state === "working" ? "prompt" : "stop");
1562
+ });
1347
1563
  }
1348
1564
  /** A tool invocation is an honest working signal if no status event was seen yet. */
1349
1565
  targetSelected(sessionId) {
1350
- return this.observeState(sessionId, this.states.get(sessionId) ?? "working");
1566
+ if (this.disposed) return this.tail;
1567
+ if (!this.states.has(sessionId)) this.states.set(sessionId, "working");
1568
+ return this.enqueue(() => this.publish(sessionId));
1569
+ }
1570
+ ask(sessionId, data = { kind: "permission" }) {
1571
+ if (this.disposed) return this.tail;
1572
+ return this.enqueue(async () => {
1573
+ await this.publish(sessionId);
1574
+ await this.emit(sessionId, "ask", data);
1575
+ });
1576
+ }
1577
+ toolStart(sessionId, toolName, toolUseId) {
1578
+ if (this.disposed) return this.tail;
1579
+ const data = {
1580
+ tool_name: toolName,
1581
+ ...toolUseId === void 0 ? {} : { tool_use_id: toolUseId }
1582
+ };
1583
+ return this.enqueue(async () => {
1584
+ await this.publish(sessionId);
1585
+ await this.emit(sessionId, "tool_start", data);
1586
+ });
1587
+ }
1588
+ toolEnd(sessionId, toolName, toolUseId) {
1589
+ if (this.disposed) return this.tail;
1590
+ const data = {
1591
+ tool_name: toolName,
1592
+ ...toolUseId === void 0 ? {} : { tool_use_id: toolUseId }
1593
+ };
1594
+ return this.enqueue(async () => {
1595
+ await this.publish(sessionId);
1596
+ await this.emit(sessionId, "tool_end", data);
1597
+ });
1351
1598
  }
1352
1599
  deleteSession(sessionId) {
1353
1600
  this.states.delete(sessionId);
1354
1601
  return this.enqueue(async () => {
1602
+ await this.finishSession(sessionId);
1355
1603
  await this.clearSession(sessionId);
1356
1604
  });
1357
1605
  }
@@ -1359,10 +1607,11 @@ var OpenCodeLifecycle = class {
1359
1607
  if (this.disposed) return this.tail;
1360
1608
  this.disposed = true;
1361
1609
  this.states.clear();
1362
- const sessions = [...this.owned.keys()];
1610
+ const sessions = [.../* @__PURE__ */ new Set([...this.owned.keys(), ...this.sessions.keys()])];
1363
1611
  this.enqueue(async () => {
1364
1612
  for (const sessionId of sessions) {
1365
1613
  try {
1614
+ await this.finishSession(sessionId);
1366
1615
  await this.clearSession(sessionId);
1367
1616
  } catch (error) {
1368
1617
  this.onError(error);
@@ -1388,7 +1637,11 @@ var OpenCodeLifecycle = class {
1388
1637
  await this.clearOwned(previous);
1389
1638
  this.owned.delete(sessionId);
1390
1639
  }
1391
- if (target === void 0) return;
1640
+ if (target === void 0) {
1641
+ await this.emitter(sessionId).bind(null, sessionId, this.execution());
1642
+ return;
1643
+ }
1644
+ await this.bindSession(sessionId, target);
1392
1645
  if (previous !== void 0 && previous.target === target) return;
1393
1646
  const binding = { target, owner: `opencode:${sessionId}` };
1394
1647
  this.owned.set(sessionId, binding);
@@ -1416,6 +1669,43 @@ var OpenCodeLifecycle = class {
1416
1669
  execution() {
1417
1670
  return { signal: new AbortController().signal, timeoutMs: this.timeoutMs };
1418
1671
  }
1672
+ emitter(sessionId) {
1673
+ const existing = this.sessions.get(sessionId);
1674
+ if (existing !== void 0) return existing;
1675
+ const created = new AgentSessionEmitter(
1676
+ hasAgentSessionCli(this.cli) ? this.cli : null,
1677
+ { provider: "opencode", onError: this.onError }
1678
+ );
1679
+ this.sessions.set(sessionId, created);
1680
+ return created;
1681
+ }
1682
+ async bindSession(sessionId, target) {
1683
+ const owner = this.openedPanes.get(target);
1684
+ if (owner !== void 0 && owner !== sessionId) return;
1685
+ const previous = [...this.openedPanes.entries()].find((entry) => entry[1] === sessionId);
1686
+ if (previous !== void 0 && previous[0] !== target) this.openedPanes.delete(previous[0]);
1687
+ const emitter = this.emitter(sessionId);
1688
+ await emitter.bind(target, sessionId, this.execution());
1689
+ if (emitter.isOpen) this.openedPanes.set(target, sessionId);
1690
+ }
1691
+ async emit(sessionId, type, data) {
1692
+ if (this.disposed) return;
1693
+ const target = this.target();
1694
+ if (target !== void 0) {
1695
+ const owner = this.openedPanes.get(target);
1696
+ if (owner !== void 0 && owner !== sessionId) return;
1697
+ }
1698
+ await this.emitter(sessionId).emit(type, data, this.execution());
1699
+ }
1700
+ async finishSession(sessionId) {
1701
+ const emitter = this.sessions.get(sessionId);
1702
+ if (emitter === void 0) return;
1703
+ await emitter.finish(this.execution());
1704
+ this.sessions.delete(sessionId);
1705
+ for (const [pane, owner] of this.openedPanes) {
1706
+ if (owner === sessionId) this.openedPanes.delete(pane);
1707
+ }
1708
+ }
1419
1709
  };
1420
1710
  function handleLifecycleEvent(lifecycle, event) {
1421
1711
  switch (event.type) {
@@ -1441,10 +1731,36 @@ function handleLifecycleEvent(lifecycle, event) {
1441
1731
  const sessionID = info !== null && typeof info === "object" ? info.id : void 0;
1442
1732
  return typeof sessionID === "string" ? lifecycle.deleteSession(sessionID) : Promise.resolve();
1443
1733
  }
1734
+ case "permission.asked": {
1735
+ const sessionID = sessionIdOf(event.properties);
1736
+ return sessionID === void 0 ? Promise.resolve() : lifecycle.ask(sessionID, permissionAskData(event.properties));
1737
+ }
1444
1738
  default:
1445
1739
  return Promise.resolve();
1446
1740
  }
1447
1741
  }
1742
+ function sessionIdOf(properties) {
1743
+ if (typeof properties.sessionID === "string") return properties.sessionID;
1744
+ if (typeof properties.sessionId === "string") return properties.sessionId;
1745
+ const info = properties.info;
1746
+ if (info !== null && typeof info === "object") {
1747
+ const id = info.id;
1748
+ if (typeof id === "string") return id;
1749
+ const nested = info.sessionID;
1750
+ if (typeof nested === "string") return nested;
1751
+ }
1752
+ return void 0;
1753
+ }
1754
+ function permissionAskData(properties) {
1755
+ const id = typeof properties.id === "string" ? properties.id : typeof properties.permissionID === "string" ? properties.permissionID : void 0;
1756
+ const permission = properties.permission;
1757
+ const question = typeof permission === "string" ? permission : typeof properties.title === "string" ? properties.title : void 0;
1758
+ return {
1759
+ kind: "permission",
1760
+ ...id === void 0 ? {} : { id },
1761
+ ...question === void 0 ? {} : { question }
1762
+ };
1763
+ }
1448
1764
  function lifecycleRecord(owner) {
1449
1765
  return { name: "opencode", kind: "opencode", session: owner };
1450
1766
  }
@@ -1720,106 +2036,144 @@ function byteLength(text) {
1720
2036
 
1721
2037
  // src/index.ts
1722
2038
  function createPhuxPlugin(defaults = {}) {
1723
- return Plugin.define({
1724
- id: "phux.terminal",
1725
- setup: async (context) => {
1726
- const options = mergeOptions(defaults, context.options);
1727
- const environment = options.env ?? process.env;
1728
- const environmentTarget = readEnvironmentTarget(environment.PHUX_TARGET);
1729
- const cli = options.cli ?? new PhuxCli(cliOptions(options, environment));
1730
- let selectedTarget;
1731
- const currentTarget = () => selectedTarget ?? environmentTarget;
1732
- const lifecycle = new OpenCodeLifecycle({
1733
- cli,
1734
- target: currentTarget,
1735
- ...options.lifecycleTimeoutMs === void 0 ? {} : { timeoutMs: options.lifecycleTimeoutMs },
1736
- ...options.onLifecycleError === void 0 ? {} : { onError: options.onLifecycleError }
1737
- });
1738
- const awareness = new PhuxContextAwareness(cli, {
1739
- enabled: options.contextAwareness ?? contextAwarenessEnabled(environment.PHUX_CONTEXT_AWARENESS),
1740
- ...options.contextTimeoutMs === void 0 ? {} : { timeoutMs: options.contextTimeoutMs }
1741
- });
1742
- const latestContext = /* @__PURE__ */ new Map();
1743
- const contextIdentity = () => {
1744
- const self = normalizeTerminalIdentity(environment.PHUX_TERMINAL_ID);
1745
- const selected = currentTarget();
1746
- return {
1747
- ...self === null ? {} : { self },
1748
- ...selected === void 0 ? {} : { selected }
1749
- };
2039
+ return async (_input, configured) => {
2040
+ const options = mergeOptions(defaults, configured ?? {});
2041
+ const environment = options.env ?? process.env;
2042
+ const environmentTarget = readEnvironmentTarget(environment.PHUX_TARGET);
2043
+ const cli = options.cli ?? new PhuxCli(cliOptions(options, environment));
2044
+ let selectedTarget;
2045
+ const currentTarget = () => selectedTarget ?? environmentTarget;
2046
+ const lifecycle = new OpenCodeLifecycle({
2047
+ cli,
2048
+ target: currentTarget,
2049
+ ...options.lifecycleTimeoutMs === void 0 ? {} : { timeoutMs: options.lifecycleTimeoutMs },
2050
+ ...options.onLifecycleError === void 0 ? {} : { onError: options.onLifecycleError }
2051
+ });
2052
+ const awareness = new PhuxContextAwareness(cli, {
2053
+ enabled: options.contextAwareness ?? contextAwarenessEnabled(environment.PHUX_CONTEXT_AWARENESS),
2054
+ ...options.contextTimeoutMs === void 0 ? {} : { timeoutMs: options.contextTimeoutMs }
2055
+ });
2056
+ const latestContext = /* @__PURE__ */ new Map();
2057
+ const contextIdentity = () => {
2058
+ const self = normalizeTerminalIdentity(environment.PHUX_TERMINAL_ID);
2059
+ const selected = currentTarget();
2060
+ return {
2061
+ ...self === null ? {} : { self },
2062
+ ...selected === void 0 ? {} : { selected }
1750
2063
  };
1751
- const tools = createPhuxTools({
1752
- cli,
1753
- ...environmentTarget === void 0 ? {} : { environmentTarget },
1754
- getSelectedTarget: () => selectedTarget,
1755
- selectTarget: (target) => {
1756
- selectedTarget = target;
1757
- },
1758
- targetSelected: (toolContext) => {
1759
- void lifecycle.targetSelected(toolContext.sessionID);
1760
- }
1761
- });
1762
- const toolRegistration = await context.tool.transform((draft) => {
1763
- for (const tool of Object.values(tools)) draft.add(tool);
1764
- });
1765
- const contextRegistration = await context.session.hook("context", async (event) => {
1766
- const emission = await awareness.next(event.sessionID, contextIdentity());
1767
- if (emission !== null) latestContext.set(event.sessionID, emission.text);
1768
- const text = latestContext.get(event.sessionID);
2064
+ };
2065
+ const tools = createPhuxTools({
2066
+ cli,
2067
+ ...environmentTarget === void 0 ? {} : { environmentTarget },
2068
+ getSelectedTarget: () => selectedTarget,
2069
+ selectTarget: (target) => {
2070
+ selectedTarget = target;
2071
+ },
2072
+ targetSelected: (toolContext) => {
2073
+ void lifecycle.targetSelected(toolContext.sessionID);
2074
+ }
2075
+ });
2076
+ return {
2077
+ tool: openCodeTools(tools),
2078
+ "experimental.chat.system.transform": async (input, output) => {
2079
+ if (input.sessionID === void 0) return;
2080
+ const emission = await awareness.next(input.sessionID, contextIdentity());
2081
+ if (emission !== null) latestContext.set(input.sessionID, emission.text);
2082
+ const text = latestContext.get(input.sessionID);
1769
2083
  if (text === void 0) return;
1770
- event.system.push({
1771
- type: "text",
1772
- text,
1773
- metadata: { phuxContext: true }
1774
- });
1775
- });
1776
- const controller = new AbortController();
1777
- const eventTask = consumeEvents(
1778
- context.event.subscribe({ signal: controller.signal }),
1779
- lifecycle,
1780
- awareness,
1781
- latestContext,
1782
- options.onLifecycleError
1783
- );
1784
- return async () => {
1785
- controller.abort();
1786
- await Promise.all([
1787
- toolRegistration.dispose(),
1788
- contextRegistration.dispose(),
1789
- eventTask,
1790
- lifecycle.dispose()
1791
- ]);
1792
- };
1793
- }
1794
- });
1795
- }
1796
- var PhuxPlugin = createPhuxPlugin();
1797
- var index_default = PhuxPlugin;
1798
- async function consumeEvents(events, lifecycle, awareness, latestContext, onError) {
1799
- try {
1800
- for await (const event of events) {
1801
- if (!isLifecycleEvent(event)) continue;
1802
- await handleLifecycleEvent(lifecycle, event);
1803
- if (event.type === "session.deleted") {
2084
+ output.system.push(text);
2085
+ },
2086
+ "tool.execute.before": async (input) => {
2087
+ await lifecycle.toolStart(input.sessionID, input.tool, input.callID);
2088
+ },
2089
+ "tool.execute.after": async (input) => {
2090
+ await lifecycle.toolEnd(input.sessionID, input.tool, input.callID);
2091
+ },
2092
+ "permission.ask": async (input) => {
2093
+ const sessionID = input.sessionID;
2094
+ if (typeof sessionID !== "string") return;
2095
+ await lifecycle.ask(sessionID, { kind: "permission" });
2096
+ },
2097
+ event: async ({ event }) => {
2098
+ if (!isLifecycleEvent(event)) return;
2099
+ await handleLifecycleEvent(lifecycle, event);
2100
+ if (event.type !== "session.deleted") return;
1804
2101
  const info = event.properties.info;
1805
2102
  const sessionID = info !== null && typeof info === "object" ? info.id : void 0;
1806
- if (typeof sessionID !== "string") continue;
2103
+ if (typeof sessionID !== "string") return;
1807
2104
  awareness.delete(sessionID);
1808
2105
  latestContext.delete(sessionID);
1809
- }
2106
+ },
2107
+ dispose: async () => lifecycle.dispose()
2108
+ };
2109
+ };
2110
+ }
2111
+ var PhuxPlugin = createPhuxPlugin();
2112
+ var index_default = PhuxPlugin;
2113
+ var z = tool.schema;
2114
+ var localTimeoutArgument = () => z.number().int().min(1).max(36e5).optional();
2115
+ var targetArgument = () => nonBlankString(512).optional();
2116
+ function nonBlankString(maxLength) {
2117
+ return z.string().min(1).max(maxLength).regex(/\S/);
2118
+ }
2119
+ function openCodeTools(tools) {
2120
+ return {
2121
+ phux_list: adaptTool(tools.phux_list, { local_timeout_ms: localTimeoutArgument() }),
2122
+ phux_create: adaptTool(tools.phux_create, {
2123
+ name: nonBlankString(255),
2124
+ cwd: nonBlankString(4096).optional(),
2125
+ command: z.array(z.string().max(65536)).min(1).max(256).optional(),
2126
+ local_timeout_ms: localTimeoutArgument()
2127
+ }),
2128
+ phux_snapshot: adaptTool(tools.phux_snapshot, {
2129
+ target: targetArgument(),
2130
+ scrollback: z.number().int().min(0).max(1e5).optional(),
2131
+ cells: z.boolean().optional(),
2132
+ local_timeout_ms: localTimeoutArgument()
2133
+ }),
2134
+ phux_send_keys: adaptTool(tools.phux_send_keys, {
2135
+ target: targetArgument(),
2136
+ keys: z.array(nonBlankString(65536)).min(1).max(256),
2137
+ local_timeout_ms: localTimeoutArgument()
2138
+ }),
2139
+ phux_run: adaptTool(tools.phux_run, {
2140
+ target: targetArgument(),
2141
+ command: nonBlankString(65536),
2142
+ timeout_seconds: z.number().int().min(0).max(86400).optional(),
2143
+ local_timeout_ms: localTimeoutArgument()
2144
+ }),
2145
+ phux_wait: adaptTool(tools.phux_wait, {
2146
+ target: targetArgument(),
2147
+ until: nonBlankString(4096).optional(),
2148
+ idle_ms: z.number().int().min(0).max(864e5).optional(),
2149
+ timeout_seconds: z.number().int().min(1).max(86400).optional(),
2150
+ local_timeout_ms: localTimeoutArgument()
2151
+ })
2152
+ };
2153
+ }
2154
+ function adaptTool(definition, args) {
2155
+ return tool({
2156
+ description: definition.description,
2157
+ args,
2158
+ execute: async (input, context) => {
2159
+ const result = await definition.execute(input, phuxToolContext(context));
2160
+ return { output: result.content, metadata: result.metadata };
1810
2161
  }
1811
- } catch (error) {
1812
- if (!isAbortError(error)) onError?.(error);
1813
- }
2162
+ });
2163
+ }
2164
+ function phuxToolContext(context) {
2165
+ return {
2166
+ sessionID: context.sessionID,
2167
+ messageID: context.messageID,
2168
+ agent: context.agent,
2169
+ id: context.messageID
2170
+ };
1814
2171
  }
1815
2172
  function isLifecycleEvent(value) {
1816
2173
  if (value === null || typeof value !== "object") return false;
1817
2174
  const candidate = value;
1818
2175
  return typeof candidate.type === "string" && candidate.properties !== null && typeof candidate.properties === "object";
1819
2176
  }
1820
- function isAbortError(error) {
1821
- return error instanceof DOMException && error.name === "AbortError";
1822
- }
1823
2177
  function mergeOptions(defaults, configured) {
1824
2178
  return { ...defaults, ...configured };
1825
2179
  }
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@phux/opencode",
3
- "version": "0.2.1",
4
- "description": "OpenCode V2 tools and fleet context for shared phux terminals",
3
+ "version": "0.3.0",
4
+ "description": "OpenCode tools and fleet context for shared phux terminals",
5
5
  "type": "module",
6
- "license": "MIT OR Apache-2.0",
6
+ "license": "Apache-2.0",
7
7
  "repository": {
8
8
  "type": "git",
9
9
  "url": "https://github.com/no-phux/phux.git",
@@ -29,19 +29,19 @@
29
29
  "build": "npm run clean && tsup src/index.ts --format esm --platform node --dts --external @opencode-ai/plugin",
30
30
  "typecheck": "tsc -p tsconfig.json --noEmit",
31
31
  "test": "node --test test/*.test.mjs",
32
- "gates": "npm run typecheck && npm run build && npm test && npm run pack:check && npm audit --audit-level=high",
32
+ "gates": "npm run typecheck && npm run build && npm test && npm run pack:check && node ../../scripts/npm-audit-gate.mjs --audit-level=high",
33
33
  "smoke:opencode": "node scripts/smoke-opencode.mjs",
34
34
  "smoke:real": "node scripts/smoke-real.mjs",
35
35
  "pack:check": "node scripts/test-pack.mjs",
36
36
  "prepack": "npm run build"
37
37
  },
38
38
  "dependencies": {
39
- "@opencode-ai/plugin": "0.0.0-next-17430"
39
+ "@opencode-ai/plugin": "1.18.30"
40
40
  },
41
41
  "devDependencies": {
42
- "@types/node": "24.12.2",
42
+ "@types/node": "26.5.0",
43
43
  "tsup": "8.5.1",
44
- "typescript": "5.9.2"
44
+ "typescript": "6.0.3"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=22"