@poncho-ai/cli 0.40.1 → 0.40.3

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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/cli@0.40.1 build /home/runner/work/poncho-ai/poncho-ai/packages/cli
2
+ > @poncho-ai/cli@0.40.3 build /home/runner/work/poncho-ai/poncho-ai/packages/cli
3
3
  > tsup src/index.ts src/cli.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/cli.ts, src/index.ts
@@ -7,12 +7,12 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/cli.js 94.00 B
10
+ ESM dist/cli.js 528.00 B
11
11
  ESM dist/index.js 3.10 KB
12
- ESM dist/run-interactive-ink-LJTKUUV4.js 23.38 KB
13
- ESM dist/chunk-KVGMTYDD.js 674.92 KB
14
- ESM ⚡️ Build success in 72ms
12
+ ESM dist/run-interactive-ink-JWYEHJZH.js 23.38 KB
13
+ ESM dist/chunk-7YEM6KJS.js 665.12 KB
14
+ ESM ⚡️ Build success in 73ms
15
15
  DTS Build start
16
- DTS ⚡️ Build success in 4002ms
16
+ DTS ⚡️ Build success in 4142ms
17
17
  DTS dist/cli.d.ts 20.00 B
18
18
  DTS dist/index.d.ts 13.25 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
1
  # @poncho-ai/cli
2
2
 
3
+ ## 0.40.3
4
+
5
+ ### Patch Changes
6
+
7
+ - [`111d24e`](https://github.com/cesr/poncho-ai/commit/111d24efaab054ef7543c396085f8f4d41e7976a) Thanks [@cesr](https://github.com/cesr)! - cli: include VFS skills in the chat input slash command menu
8
+
9
+ The `/api/slash-commands` endpoint was returning only repo-loaded skills,
10
+ so tenant-authored skills stored in the VFS (`/skills/<name>/SKILL.md`)
11
+ never appeared in the `/` autocomplete bar even though the agent could
12
+ already see and run them at conversation time.
13
+
14
+ The endpoint now resolves skills per-tenant via a new
15
+ `harness.listSkillsForTenant(tenantId)` and applies the same repo-wins
16
+ collision semantics used elsewhere in the harness.
17
+
18
+ - [`39793b0`](https://github.com/cesr/poncho-ai/commit/39793b0ab11ed26f140af6fc9c0cd3e1b1c83fec) Thanks [@cesr](https://github.com/cesr)! - harness: extract `runConversationTurn` helper; refactor CLI to use it
19
+
20
+ Lifts the inline turn lifecycle from the CLI's
21
+ `POST /api/conversations/:id/messages` handler (~280 lines of orchestration)
22
+ into a new public helper at `@poncho-ai/harness`.
23
+
24
+ The helper handles the full conversation lifecycle for a primary chat
25
+ turn: load the conversation with archive, resolve canonical history,
26
+ upload files via the harness's upload store, build stable user/assistant
27
+ ids, persist the user message immediately, drive `executeConversationTurn`,
28
+ periodically persist the in-flight assistant draft on `step:completed`
29
+ and `tool:approval:required`, persist on `tool:approval:checkpoint` and
30
+ `run:completed` continuation, rebuild history on `compaction:completed`,
31
+ apply turn metadata on success, and persist partial state on
32
+ cancel/error.
33
+
34
+ Caller responsibilities (auth, active-run dedup, streaming, continuation
35
+ HTTP self-fetch, title inference) stay outside the helper — passed in
36
+ via opts or handled around the call. `opts.onEvent` is invoked for every
37
+ `AgentEvent` for downstream forwarding (SSE, WebSocket, telemetry, etc.).
38
+
39
+ The CLI's handler now delegates to `runConversationTurn` (drops from
40
+ ~430 to ~150 lines). Consumers like PonchOS can call the same helper
41
+ to ship the _exact_ same conversation lifecycle without duplicating
42
+ the orchestration.
43
+
44
+ Public API additions:
45
+ - `runConversationTurn(opts): Promise<RunConversationTurnResult>`
46
+ - `RunConversationTurnOpts`
47
+ - `RunConversationTurnResult`
48
+
49
+ No behavior changes. The helper is a verbatim extraction of the CLI's
50
+ prior inline implementation.
51
+
52
+ - Updated dependencies [[`111d24e`](https://github.com/cesr/poncho-ai/commit/111d24efaab054ef7543c396085f8f4d41e7976a), [`39793b0`](https://github.com/cesr/poncho-ai/commit/39793b0ab11ed26f140af6fc9c0cd3e1b1c83fec)]:
53
+ - @poncho-ai/harness@0.42.0
54
+
55
+ ## 0.40.2
56
+
57
+ ### Patch Changes
58
+
59
+ - [`1dd0e5b`](https://github.com/cesr/poncho-ai/commit/1dd0e5b98f93de5715614be97e62ed503792cf16) Thanks [@cesr](https://github.com/cesr)! - cli: suppress Node `ExperimentalWarning` output during `poncho dev`
60
+
61
+ When running on Node 22.6+, every `.ts` skill script triggers a
62
+ `stripTypeScriptTypes is an experimental feature` warning via
63
+ `process.emitWarning`. Repeated activation of TypeScript-backed skills
64
+ spammed the dev server log with the same warning, sometimes 4–8 times
65
+ in a single agent turn.
66
+
67
+ The CLI now installs an in-process `process.emitWarning` filter at the
68
+ entry point that drops `ExperimentalWarning`s before they reach stderr.
69
+ Other warnings (deprecation, security, etc.) pass through unchanged.
70
+
71
+ If the in-process filter doesn't catch a particular warning (e.g. one
72
+ emitted from a Node internal module before user code runs), users can
73
+ still suppress them with `NODE_OPTIONS='--disable-warning=ExperimentalWarning'`.
74
+
75
+ - Updated dependencies [[`7d57a88`](https://github.com/cesr/poncho-ai/commit/7d57a88e55a49ec04de3dbd415b2440bb727e31f), [`ac18616`](https://github.com/cesr/poncho-ai/commit/ac18616b864189c91d0957c72c537933497505f4), [`4b5d974`](https://github.com/cesr/poncho-ai/commit/4b5d974345733ac9e68f36201dff7e7d8a8f0327), [`c22416b`](https://github.com/cesr/poncho-ai/commit/c22416b3d4c4557277aeabf53e70877be6436e85)]:
76
+ - @poncho-ai/harness@0.41.0
77
+
3
78
  ## 0.40.1
4
79
 
5
80
  ### Patch Changes
@@ -14,7 +14,6 @@ import {
14
14
  createConversationStore as createConversationStore2,
15
15
  createConversationStoreFromEngine as createConversationStoreFromEngine2,
16
16
  createUploadStore as createUploadStore2,
17
- deriveUploadKey,
18
17
  ensureAgentIdentity as ensureAgentIdentity3,
19
18
  loadPonchoConfig as loadPonchoConfig4,
20
19
  parseAgentMarkdown as parseAgentMarkdown2,
@@ -32,6 +31,7 @@ import {
32
31
  normalizeApprovalCheckpoint,
33
32
  buildApprovalCheckpoints,
34
33
  applyTurnMetadata,
34
+ runConversationTurn,
35
35
  withToolResultArchiveParam,
36
36
  AgentOrchestrator
37
37
  } from "@poncho-ai/harness";
@@ -571,7 +571,7 @@ var WEB_UI_STYLES = `
571
571
  .sidebar-segmented {
572
572
  display: inline-flex;
573
573
  align-self: stretch;
574
- margin: 12px 6px 0;
574
+ margin: 12px 6px 8px;
575
575
  padding: 3px;
576
576
  background: var(--surface-3);
577
577
  border-radius: 999px;
@@ -14261,7 +14261,7 @@ var runInteractive = async (workingDir, params) => {
14261
14261
  await harness.initialize();
14262
14262
  const identity = await ensureAgentIdentity2(workingDir);
14263
14263
  try {
14264
- const { runInteractiveInk } = await import("./run-interactive-ink-LJTKUUV4.js");
14264
+ const { runInteractiveInk } = await import("./run-interactive-ink-JWYEHJZH.js");
14265
14265
  await runInteractiveInk({
14266
14266
  harness,
14267
14267
  params,
@@ -14307,7 +14307,6 @@ var approvalLog = createLogger("approval");
14307
14307
  var browserLog = createLogger("browser");
14308
14308
  var selfFetchLog = createLogger("self-fetch");
14309
14309
  var csrfLog = createLogger("csrf");
14310
- var uploadLog = createLogger("upload");
14311
14310
  var collectToolCallIds = (msgs) => {
14312
14311
  const ids = /* @__PURE__ */ new Set();
14313
14312
  for (const m of msgs) {
@@ -16826,7 +16825,8 @@ data: ${JSON.stringify(frame)}
16826
16825
  return;
16827
16826
  }
16828
16827
  if (pathname === "/api/slash-commands" && request.method === "GET") {
16829
- const skills = harness.listSkills().map((s) => ({
16828
+ const tenantSkills = await harness.listSkillsForTenant(ctx.tenantId);
16829
+ const skills = tenantSkills.map((s) => ({
16830
16830
  command: "/" + s.name,
16831
16831
  description: s.description,
16832
16832
  type: "skill"
@@ -16964,7 +16964,7 @@ data: ${JSON.stringify(frame)}
16964
16964
  const conversationMessageMatch = pathname.match(/^\/api\/conversations\/([^/]+)\/messages$/);
16965
16965
  if (conversationMessageMatch && request.method === "POST") {
16966
16966
  const conversationId = decodeURIComponent(conversationMessageMatch[1] ?? "");
16967
- const conversation = await conversationStore.getWithArchive(conversationId);
16967
+ const conversation = await conversationStore.get(conversationId);
16968
16968
  if (!conversation || !canAccessConversation(conversation)) {
16969
16969
  writeJson(response, 404, {
16970
16970
  code: "CONVERSATION_NOT_FOUND",
@@ -17025,6 +17025,8 @@ data: ${JSON.stringify(frame)}
17025
17025
  });
17026
17026
  if (conversation.messages.length === 0 && (conversation.title === "New conversation" || conversation.title.trim().length === 0)) {
17027
17027
  conversation.title = inferConversationTitle(messageText);
17028
+ conversation.updatedAt = Date.now();
17029
+ await conversationStore.update(conversation);
17028
17030
  }
17029
17031
  response.writeHead(200, {
17030
17032
  "Content-Type": "text/event-stream",
@@ -17032,53 +17034,6 @@ data: ${JSON.stringify(frame)}
17032
17034
  Connection: "keep-alive",
17033
17035
  "X-Accel-Buffering": "no"
17034
17036
  });
17035
- const canonicalHistory = resolveRunRequest(conversation, {
17036
- conversationId,
17037
- messages: conversation.messages
17038
- });
17039
- const shouldRebuildCanonical = canonicalHistory.shouldRebuildCanonical;
17040
- const harnessMessages = [...canonicalHistory.messages];
17041
- const historyMessages = [...conversation.messages];
17042
- const preRunMessages = [...conversation.messages];
17043
- log.debug(
17044
- `conversation=${conversationId.slice(0, 8)} history=${canonicalHistory.source}`
17045
- );
17046
- let latestRunId = conversation.runtimeRunId ?? "";
17047
- let userContent = messageText;
17048
- if (files.length > 0) {
17049
- try {
17050
- const uploadedParts = await Promise.all(
17051
- files.map(async (f) => {
17052
- const buf = Buffer.from(f.data, "base64");
17053
- const key = deriveUploadKey(buf, f.mediaType);
17054
- const ref = await uploadStore.put(key, buf, f.mediaType);
17055
- return {
17056
- type: "file",
17057
- data: ref,
17058
- mediaType: f.mediaType,
17059
- filename: f.filename
17060
- };
17061
- })
17062
- );
17063
- userContent = [
17064
- { type: "text", text: messageText },
17065
- ...uploadedParts
17066
- ];
17067
- } catch (uploadErr) {
17068
- const errMsg = uploadErr instanceof Error ? uploadErr.message : String(uploadErr);
17069
- uploadLog.error(`file upload failed: ${errMsg}`);
17070
- const errorEvent = {
17071
- type: "run:error",
17072
- runId: "",
17073
- error: { code: "UPLOAD_ERROR", message: `File upload failed: ${errMsg}` }
17074
- };
17075
- broadcastEvent(conversationId, errorEvent);
17076
- finishConversationStream(conversationId);
17077
- activeConversationRuns.delete(conversationId);
17078
- response.end();
17079
- return;
17080
- }
17081
- }
17082
17037
  const unsubSubagentEvents = onConversationEvent(conversationId, (evt) => {
17083
17038
  if (evt.type.startsWith("subagent:")) {
17084
17039
  try {
@@ -17087,79 +17042,18 @@ data: ${JSON.stringify(frame)}
17087
17042
  }
17088
17043
  }
17089
17044
  });
17090
- const draft = createTurnDraftState();
17091
- let checkpointedRun = false;
17092
- let runCancelled = false;
17093
- let runContinuationMessages;
17094
- let cancelHarnessMessages;
17095
- const turnTimestamp = Date.now();
17096
- const userMessage = userContent != null ? {
17097
- role: "user",
17098
- content: userContent,
17099
- metadata: { id: randomUUID3(), timestamp: turnTimestamp }
17100
- } : void 0;
17101
- const assistantId = randomUUID3();
17102
- const buildMessages = () => {
17103
- const draftSections = cloneSections(draft.sections);
17104
- if (draft.currentTools.length > 0) {
17105
- draftSections.push({ type: "tools", content: [...draft.currentTools] });
17106
- }
17107
- if (draft.currentText.length > 0) {
17108
- draftSections.push({ type: "text", content: draft.currentText });
17109
- }
17110
- const userTurn = userMessage ? [userMessage] : [];
17111
- const hasDraftContent = draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draftSections.length > 0;
17112
- if (!hasDraftContent) {
17113
- return [...historyMessages, ...userTurn];
17114
- }
17115
- return [
17116
- ...historyMessages,
17117
- ...userTurn,
17118
- {
17119
- role: "assistant",
17120
- content: draft.assistantResponse,
17121
- metadata: buildAssistantMetadata2(draft, draftSections, { id: assistantId, timestamp: turnTimestamp })
17122
- }
17123
- ];
17124
- };
17125
- const persistDraftAssistantTurn = async () => {
17126
- if (draft.assistantResponse.length === 0 && draft.toolTimeline.length === 0) return;
17127
- conversation.messages = buildMessages();
17128
- conversation.updatedAt = Date.now();
17129
- await conversationStore.update(conversation);
17130
- };
17045
+ let latestRunId = "";
17131
17046
  try {
17132
- {
17133
- conversation.messages = [
17134
- ...historyMessages,
17135
- ...userMessage ? [userMessage] : []
17136
- ];
17137
- conversation.subagentCallbackCount = 0;
17138
- conversation._continuationCount = void 0;
17139
- conversation.updatedAt = Date.now();
17140
- conversationStore.update(conversation).catch((err) => {
17141
- log.error(`failed to persist user turn: ${formatError(err)}`);
17142
- });
17143
- }
17144
- const execution = await executeConversationTurn2({
17047
+ const result = await runConversationTurn({
17145
17048
  harness,
17146
- runInput: {
17147
- task: messageText,
17148
- conversationId,
17149
- tenantId: ctx.tenantId ?? void 0,
17150
- parameters: buildTurnParameters(conversation, { bodyParameters }),
17151
- messages: harnessMessages,
17152
- files: files.length > 0 ? files : void 0,
17153
- abortSignal: abortController.signal
17154
- },
17155
- initialContextTokens: conversation.contextTokens ?? 0,
17156
- initialContextWindow: conversation.contextWindow ?? 0,
17157
- onEvent: async (event, eventDraft) => {
17158
- draft.assistantResponse = eventDraft.assistantResponse;
17159
- draft.toolTimeline = eventDraft.toolTimeline;
17160
- draft.sections = eventDraft.sections;
17161
- draft.currentTools = eventDraft.currentTools;
17162
- draft.currentText = eventDraft.currentText;
17049
+ conversationStore,
17050
+ conversationId,
17051
+ task: messageText,
17052
+ files: files.length > 0 ? files : void 0,
17053
+ parameters: buildTurnParameters(conversation, { bodyParameters }),
17054
+ abortSignal: abortController.signal,
17055
+ tenantId: ctx.tenantId ?? void 0,
17056
+ onEvent: async (event) => {
17163
17057
  if (event.type === "run:started") {
17164
17058
  latestRunId = event.runId;
17165
17059
  runOwners.set(event.runId, ownerId);
@@ -17169,98 +17063,12 @@ data: ${JSON.stringify(frame)}
17169
17063
  active.runId = event.runId;
17170
17064
  }
17171
17065
  }
17172
- if (event.type === "run:cancelled") {
17173
- runCancelled = true;
17174
- if (event.messages) cancelHarnessMessages = event.messages;
17175
- }
17176
- if (event.type === "compaction:completed") {
17177
- if (event.compactedMessages) {
17178
- historyMessages.length = 0;
17179
- historyMessages.push(...event.compactedMessages);
17180
- const preservedFromHistory = historyMessages.length - 1;
17181
- const removedCount = preRunMessages.length - Math.max(0, preservedFromHistory);
17182
- const existingHistory = conversation.compactedHistory ?? [];
17183
- conversation.compactedHistory = [
17184
- ...existingHistory,
17185
- ...preRunMessages.slice(0, removedCount)
17186
- ];
17187
- }
17188
- }
17189
- if (event.type === "step:completed") {
17190
- await persistDraftAssistantTurn();
17191
- }
17192
- if (event.type === "tool:approval:required") {
17193
- const toolText = `- approval required \`${event.tool}\``;
17194
- draft.toolTimeline.push(toolText);
17195
- draft.currentTools.push(toolText);
17196
- const existingApprovals = Array.isArray(conversation.pendingApprovals) ? conversation.pendingApprovals : [];
17197
- if (!existingApprovals.some((approval) => approval.approvalId === event.approvalId)) {
17198
- conversation.pendingApprovals = [
17199
- ...existingApprovals,
17200
- {
17201
- approvalId: event.approvalId,
17202
- runId: latestRunId || conversation.runtimeRunId || "",
17203
- tool: event.tool,
17204
- toolCallId: void 0,
17205
- input: event.input ?? {},
17206
- checkpointMessages: void 0,
17207
- baseMessageCount: historyMessages.length,
17208
- pendingToolCalls: []
17209
- }
17210
- ];
17211
- conversation.updatedAt = Date.now();
17212
- await conversationStore.update(conversation);
17213
- }
17214
- await persistDraftAssistantTurn();
17215
- }
17216
- if (event.type === "tool:approval:checkpoint") {
17217
- conversation.messages = buildMessages();
17218
- conversation.pendingApprovals = buildApprovalCheckpoints({
17219
- approvals: event.approvals,
17220
- runId: latestRunId,
17221
- checkpointMessages: event.checkpointMessages,
17222
- baseMessageCount: historyMessages.length,
17223
- pendingToolCalls: event.pendingToolCalls
17224
- });
17225
- conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
17226
- conversation.updatedAt = Date.now();
17227
- await conversationStore.update(conversation);
17228
- checkpointedRun = true;
17229
- }
17230
- if (event.type === "run:completed") {
17231
- if (event.result.continuation && event.result.continuationMessages) {
17232
- runContinuationMessages = event.result.continuationMessages;
17233
- conversation.messages = buildMessages();
17234
- conversation._continuationMessages = runContinuationMessages;
17235
- conversation._harnessMessages = runContinuationMessages;
17236
- conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
17237
- conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
17238
- if (!checkpointedRun) {
17239
- conversation.pendingApprovals = [];
17240
- }
17241
- if ((event.result.contextTokens ?? 0) > 0) conversation.contextTokens = event.result.contextTokens;
17242
- if ((event.result.contextWindow ?? 0) > 0) conversation.contextWindow = event.result.contextWindow;
17243
- conversation.updatedAt = Date.now();
17244
- await conversationStore.update(conversation);
17245
- if (!checkpointedRun) {
17246
- doWaitUntil(
17247
- new Promise((r) => setTimeout(r, 3e3)).then(
17248
- () => selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`)
17249
- )
17250
- );
17251
- }
17252
- }
17253
- }
17254
17066
  await telemetry.emit(event);
17255
17067
  let sseEvent = event.type === "compaction:completed" && event.compactedMessages ? { ...event, compactedMessages: void 0 } : event;
17256
17068
  if (sseEvent.type === "run:completed") {
17257
17069
  const hasPendingSubagents = await hasPendingSubagentWorkForParent(conversationId, ownerId);
17258
17070
  const stripped = { ...sseEvent, result: { ...sseEvent.result, continuationMessages: void 0 } };
17259
- if (hasPendingSubagents) {
17260
- sseEvent = { ...stripped, pendingSubagents: true };
17261
- } else {
17262
- sseEvent = stripped;
17263
- }
17071
+ sseEvent = hasPendingSubagents ? { ...stripped, pendingSubagents: true } : stripped;
17264
17072
  }
17265
17073
  broadcastEvent(conversationId, sseEvent);
17266
17074
  try {
@@ -17270,38 +17078,15 @@ data: ${JSON.stringify(frame)}
17270
17078
  emitBrowserStatusIfActive(conversationId, event, response);
17271
17079
  }
17272
17080
  });
17273
- flushTurnDraft2(draft);
17274
- latestRunId = execution.latestRunId || latestRunId;
17275
- if (!checkpointedRun && !runContinuationMessages) {
17276
- conversation.messages = buildMessages();
17277
- applyTurnMetadata(conversation, {
17278
- latestRunId,
17279
- contextTokens: execution.runContextTokens,
17280
- contextWindow: execution.runContextWindow,
17281
- harnessMessages: execution.runHarnessMessages,
17282
- toolResultArchive: harness.getToolResultArchive(conversationId)
17283
- }, { shouldRebuildCanonical });
17284
- await conversationStore.update(conversation);
17081
+ if (result.continuation && !result.checkpointed) {
17082
+ doWaitUntil(
17083
+ new Promise((r) => setTimeout(r, 3e3)).then(
17084
+ () => selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`)
17085
+ )
17086
+ );
17285
17087
  }
17286
17088
  } catch (error) {
17287
- flushTurnDraft2(draft);
17288
- if (abortController.signal.aborted || runCancelled) {
17289
- if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
17290
- conversation.messages = buildMessages();
17291
- applyTurnMetadata(conversation, {
17292
- latestRunId,
17293
- contextTokens: 0,
17294
- contextWindow: 0,
17295
- harnessMessages: cancelHarnessMessages,
17296
- toolResultArchive: harness.getToolResultArchive(conversationId)
17297
- }, { shouldRebuildCanonical: true });
17298
- await conversationStore.update(conversation);
17299
- }
17300
- if (!checkpointedRun) {
17301
- await clearPendingApprovalsForConversation(conversationId);
17302
- }
17303
- return;
17304
- }
17089
+ log.error(`runConversationTurn threw: ${formatError(error)}`);
17305
17090
  try {
17306
17091
  response.write(
17307
17092
  formatSseEvent({
@@ -17314,11 +17099,6 @@ data: ${JSON.stringify(frame)}
17314
17099
  })
17315
17100
  );
17316
17101
  } catch {
17317
- if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
17318
- conversation.messages = buildMessages();
17319
- conversation.updatedAt = Date.now();
17320
- await conversationStore.update(conversation);
17321
- }
17322
17102
  }
17323
17103
  } finally {
17324
17104
  unsubSubagentEvents();
package/dist/cli.js CHANGED
@@ -1,7 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  main
4
- } from "./chunk-KVGMTYDD.js";
4
+ } from "./chunk-7YEM6KJS.js";
5
5
 
6
6
  // src/cli.ts
7
+ var _originalEmitWarning = process.emitWarning.bind(process);
8
+ process.emitWarning = ((warning, ...args) => {
9
+ const name = typeof warning === "object" && warning !== null && "name" in warning ? warning.name : typeof args[0] === "string" ? args[0] : args[0] && typeof args[0] === "object" && "type" in args[0] ? args[0].type : void 0;
10
+ if (name === "ExperimentalWarning") return;
11
+ return _originalEmitWarning(warning, ...args);
12
+ });
7
13
  void main();
package/dist/index.js CHANGED
@@ -77,7 +77,7 @@ import {
77
77
  writeHtml,
78
78
  writeJson,
79
79
  writeScaffoldFile
80
- } from "./chunk-KVGMTYDD.js";
80
+ } from "./chunk-7YEM6KJS.js";
81
81
  export {
82
82
  AGENT_TEMPLATE,
83
83
  ENV_TEMPLATE,
@@ -3,7 +3,7 @@ import {
3
3
  getMascotLines,
4
4
  inferConversationTitle,
5
5
  resolveHarnessEnvironment
6
- } from "./chunk-KVGMTYDD.js";
6
+ } from "./chunk-7YEM6KJS.js";
7
7
 
8
8
  // src/run-interactive-ink.ts
9
9
  import * as readline from "readline";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/cli",
3
- "version": "0.40.1",
3
+ "version": "0.40.3",
4
4
  "description": "CLI for building and deploying AI agents",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,9 +28,9 @@
28
28
  "react": "^19.2.4",
29
29
  "react-devtools-core": "^6.1.5",
30
30
  "yaml": "^2.8.1",
31
+ "@poncho-ai/harness": "0.42.0",
31
32
  "@poncho-ai/messaging": "0.8.5",
32
- "@poncho-ai/sdk": "1.10.0",
33
- "@poncho-ai/harness": "0.40.1"
33
+ "@poncho-ai/sdk": "1.10.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/busboy": "^1.5.4",
package/src/cli.ts CHANGED
@@ -1,4 +1,23 @@
1
1
  #!/usr/bin/env node
2
+ // Suppress Node's `ExperimentalWarning` output. The TS-stripping warnings
3
+ // (emitted every time jiti loads a .ts skill script under Node 22.6+) are
4
+ // pure noise for poncho users — there's nothing actionable. Filter via
5
+ // `process.emitWarning` so warnings still go to telemetry / logs but don't
6
+ // pollute the dev server output.
7
+ const _originalEmitWarning = process.emitWarning.bind(process);
8
+ process.emitWarning = ((warning: unknown, ...args: unknown[]) => {
9
+ const name =
10
+ typeof warning === "object" && warning !== null && "name" in warning
11
+ ? (warning as { name?: unknown }).name
12
+ : typeof args[0] === "string"
13
+ ? args[0]
14
+ : args[0] && typeof args[0] === "object" && "type" in args[0]
15
+ ? (args[0] as { type?: unknown }).type
16
+ : undefined;
17
+ if (name === "ExperimentalWarning") return;
18
+ return (_originalEmitWarning as (...a: unknown[]) => void)(warning, ...args);
19
+ }) as typeof process.emitWarning;
20
+
2
21
  import { main } from "./index.js";
3
22
 
4
23
  void main();
package/src/index.ts CHANGED
@@ -16,7 +16,6 @@ import {
16
16
  createConversationStore,
17
17
  createConversationStoreFromEngine,
18
18
  createUploadStore,
19
- deriveUploadKey,
20
19
  ensureAgentIdentity,
21
20
  loadPonchoConfig,
22
21
  parseAgentMarkdown,
@@ -39,6 +38,7 @@ import {
39
38
  normalizeApprovalCheckpoint,
40
39
  buildApprovalCheckpoints,
41
40
  applyTurnMetadata,
41
+ runConversationTurn,
42
42
  TOOL_RESULT_ARCHIVE_PARAM,
43
43
  withToolResultArchiveParam,
44
44
  AgentOrchestrator,
@@ -93,7 +93,6 @@ const approvalLog = createLogger("approval");
93
93
  const browserLog = createLogger("browser");
94
94
  const selfFetchLog = createLogger("self-fetch");
95
95
  const csrfLog = createLogger("csrf");
96
- const uploadLog = createLogger("upload");
97
96
 
98
97
  /**
99
98
  * Walk a sequence of harness messages and collect all tool-call ids that
@@ -3209,7 +3208,8 @@ export const createRequestHandler = async (options?: {
3209
3208
  }
3210
3209
 
3211
3210
  if (pathname === "/api/slash-commands" && request.method === "GET") {
3212
- const skills: ApiSlashCommand[] = harness.listSkills().map((s) => ({
3211
+ const tenantSkills = await harness.listSkillsForTenant(ctx.tenantId);
3212
+ const skills: ApiSlashCommand[] = tenantSkills.map((s) => ({
3213
3213
  command: "/" + s.name,
3214
3214
  description: s.description,
3215
3215
  type: "skill" as const,
@@ -3353,9 +3353,9 @@ export const createRequestHandler = async (options?: {
3353
3353
  const conversationMessageMatch = pathname.match(/^\/api\/conversations\/([^/]+)\/messages$/);
3354
3354
  if (conversationMessageMatch && request.method === "POST") {
3355
3355
  const conversationId = decodeURIComponent(conversationMessageMatch[1] ?? "");
3356
- // getWithArchive conversation feeds withToolResultArchiveParam when
3357
- // the turn below calls executeConversationTurn.
3358
- const conversation = await conversationStore.getWithArchive(conversationId);
3356
+ // Light access check first; the helper reloads the conversation
3357
+ // (with archive) when it actually runs the turn.
3358
+ const conversation = await conversationStore.get(conversationId);
3359
3359
  if (!conversation || !canAccessConversation(conversation)) {
3360
3360
  writeJson(response, 404, {
3361
3361
  code: "CONVERSATION_NOT_FOUND",
@@ -3420,159 +3420,46 @@ export const createRequestHandler = async (options?: {
3420
3420
  abortController,
3421
3421
  runId: null,
3422
3422
  });
3423
+
3424
+ // Auto-infer a title for fresh conversations. Persist it before the
3425
+ // helper reloads, so its in-memory copy carries the new title.
3423
3426
  if (
3424
3427
  conversation.messages.length === 0 &&
3425
3428
  (conversation.title === "New conversation" || conversation.title.trim().length === 0)
3426
3429
  ) {
3427
3430
  conversation.title = inferConversationTitle(messageText);
3431
+ conversation.updatedAt = Date.now();
3432
+ await conversationStore.update(conversation);
3428
3433
  }
3434
+
3429
3435
  response.writeHead(200, {
3430
3436
  "Content-Type": "text/event-stream",
3431
3437
  "Cache-Control": "no-cache",
3432
3438
  Connection: "keep-alive",
3433
3439
  "X-Accel-Buffering": "no",
3434
3440
  });
3435
- const canonicalHistory = resolveRunRequest(conversation, {
3436
- conversationId,
3437
- messages: conversation.messages,
3438
- });
3439
- const shouldRebuildCanonical = canonicalHistory.shouldRebuildCanonical;
3440
- const harnessMessages = [...canonicalHistory.messages];
3441
- const historyMessages = [...conversation.messages];
3442
- const preRunMessages = [...conversation.messages];
3443
- log.debug(
3444
- `conversation=${conversationId.slice(0, 8)} history=${canonicalHistory.source}`,
3445
- );
3446
- let latestRunId = conversation.runtimeRunId ?? "";
3447
- let userContent: Message["content"] | undefined = messageText;
3448
- if (files.length > 0) {
3449
- try {
3450
- const uploadedParts = await Promise.all(
3451
- files.map(async (f) => {
3452
- const buf = Buffer.from(f.data, "base64");
3453
- const key = deriveUploadKey(buf, f.mediaType);
3454
- const ref = await uploadStore.put(key, buf, f.mediaType);
3455
- return {
3456
- type: "file" as const,
3457
- data: ref,
3458
- mediaType: f.mediaType,
3459
- filename: f.filename,
3460
- };
3461
- }),
3462
- );
3463
- userContent = [
3464
- { type: "text" as const, text: messageText },
3465
- ...uploadedParts,
3466
- ];
3467
- } catch (uploadErr) {
3468
- const errMsg = uploadErr instanceof Error ? uploadErr.message : String(uploadErr);
3469
- uploadLog.error(`file upload failed: ${errMsg}`);
3470
- const errorEvent: AgentEvent = {
3471
- type: "run:error",
3472
- runId: "",
3473
- error: { code: "UPLOAD_ERROR", message: `File upload failed: ${errMsg}` },
3474
- };
3475
- broadcastEvent(conversationId, errorEvent);
3476
- finishConversationStream(conversationId);
3477
- activeConversationRuns.delete(conversationId);
3478
- response.end();
3479
- return;
3480
- }
3481
- }
3441
+
3442
+ // Forward subagent events out-of-band — they're emitted from
3443
+ // child-conversation runs, not yielded by harness.run for this one.
3482
3444
  const unsubSubagentEvents = onConversationEvent(conversationId, (evt) => {
3483
3445
  if (evt.type.startsWith("subagent:")) {
3484
3446
  try { response.write(formatSseEvent(evt)); } catch {}
3485
3447
  }
3486
3448
  });
3487
3449
 
3488
- const draft = createTurnDraftState();
3489
- let checkpointedRun = false;
3490
- let runCancelled = false;
3491
- let runContinuationMessages: Message[] | undefined;
3492
- // Snapshot of the harness's in-flight messages emitted with run:cancelled,
3493
- // so the catch-path (executeConversationTurn threw) can still persist a
3494
- // canonical history that includes the cancelled work.
3495
- let cancelHarnessMessages: Message[] | undefined;
3496
-
3497
- // Hoist stable ids for this turn. The same userMessage / assistantId is
3498
- // reused across every buildMessages() call so the in-flight assistant
3499
- // bubble keeps a stable metadata.id from its very first persisted byte.
3500
- const turnTimestamp = Date.now();
3501
- const userMessage: Message | undefined = userContent != null
3502
- ? {
3503
- role: "user" as const,
3504
- content: userContent,
3505
- metadata: { id: randomUUID(), timestamp: turnTimestamp },
3506
- }
3507
- : undefined;
3508
- const assistantId = randomUUID();
3509
-
3510
- const buildMessages = (): Message[] => {
3511
- const draftSections = cloneSections(draft.sections);
3512
- if (draft.currentTools.length > 0) {
3513
- draftSections.push({ type: "tools", content: [...draft.currentTools] });
3514
- }
3515
- if (draft.currentText.length > 0) {
3516
- draftSections.push({ type: "text", content: draft.currentText });
3517
- }
3518
- const userTurn: Message[] = userMessage ? [userMessage] : [];
3519
- const hasDraftContent =
3520
- draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draftSections.length > 0;
3521
- if (!hasDraftContent) {
3522
- return [...historyMessages, ...userTurn];
3523
- }
3524
- return [
3525
- ...historyMessages,
3526
- ...userTurn,
3527
- {
3528
- role: "assistant" as const,
3529
- content: draft.assistantResponse,
3530
- metadata: buildAssistantMetadata(draft, draftSections, { id: assistantId, timestamp: turnTimestamp }),
3531
- },
3532
- ];
3533
- };
3534
-
3535
- const persistDraftAssistantTurn = async (): Promise<void> => {
3536
- if (draft.assistantResponse.length === 0 && draft.toolTimeline.length === 0) return;
3537
- conversation.messages = buildMessages();
3538
- conversation.updatedAt = Date.now();
3539
- await conversationStore.update(conversation);
3540
- };
3450
+ let latestRunId = "";
3541
3451
 
3542
3452
  try {
3543
- {
3544
- conversation.messages = [
3545
- ...historyMessages,
3546
- ...(userMessage ? [userMessage] : []),
3547
- ];
3548
- conversation.subagentCallbackCount = 0;
3549
- conversation._continuationCount = undefined;
3550
- conversation.updatedAt = Date.now();
3551
- conversationStore.update(conversation).catch((err) => {
3552
- log.error(`failed to persist user turn: ${formatError(err)}`);
3553
- });
3554
- }
3555
-
3556
- const execution = await executeConversationTurn({
3453
+ const result = await runConversationTurn({
3557
3454
  harness,
3558
- runInput: {
3559
- task: messageText,
3560
- conversationId,
3561
- tenantId: ctx.tenantId ?? undefined,
3562
- parameters: buildTurnParameters(conversation, { bodyParameters }),
3563
- messages: harnessMessages,
3564
- files: files.length > 0 ? files : undefined,
3565
- abortSignal: abortController.signal,
3566
- },
3567
- initialContextTokens: conversation.contextTokens ?? 0,
3568
- initialContextWindow: conversation.contextWindow ?? 0,
3569
- onEvent: async (event, eventDraft) => {
3570
- draft.assistantResponse = eventDraft.assistantResponse;
3571
- draft.toolTimeline = eventDraft.toolTimeline;
3572
- draft.sections = eventDraft.sections;
3573
- draft.currentTools = eventDraft.currentTools;
3574
- draft.currentText = eventDraft.currentText;
3575
-
3455
+ conversationStore,
3456
+ conversationId,
3457
+ task: messageText,
3458
+ files: files.length > 0 ? files : undefined,
3459
+ parameters: buildTurnParameters(conversation, { bodyParameters }),
3460
+ abortSignal: abortController.signal,
3461
+ tenantId: ctx.tenantId ?? undefined,
3462
+ onEvent: async (event) => {
3576
3463
  if (event.type === "run:started") {
3577
3464
  latestRunId = event.runId;
3578
3465
  runOwners.set(event.runId, ownerId);
@@ -3582,154 +3469,36 @@ export const createRequestHandler = async (options?: {
3582
3469
  active.runId = event.runId;
3583
3470
  }
3584
3471
  }
3585
- if (event.type === "run:cancelled") {
3586
- runCancelled = true;
3587
- if (event.messages) cancelHarnessMessages = event.messages;
3588
- }
3589
- if (event.type === "compaction:completed") {
3590
- if (event.compactedMessages) {
3591
- historyMessages.length = 0;
3592
- historyMessages.push(...event.compactedMessages);
3593
-
3594
- const preservedFromHistory = historyMessages.length - 1;
3595
- const removedCount = preRunMessages.length - Math.max(0, preservedFromHistory);
3596
- const existingHistory = conversation.compactedHistory ?? [];
3597
- conversation.compactedHistory = [
3598
- ...existingHistory,
3599
- ...preRunMessages.slice(0, removedCount),
3600
- ];
3601
- }
3602
- }
3603
- if (event.type === "step:completed") {
3604
- await persistDraftAssistantTurn();
3605
- }
3606
- if (event.type === "tool:approval:required") {
3607
- const toolText = `- approval required \`${event.tool}\``;
3608
- draft.toolTimeline.push(toolText);
3609
- draft.currentTools.push(toolText);
3610
- const existingApprovals = Array.isArray(conversation.pendingApprovals)
3611
- ? conversation.pendingApprovals
3612
- : [];
3613
- if (!existingApprovals.some((approval) => approval.approvalId === event.approvalId)) {
3614
- conversation.pendingApprovals = [
3615
- ...existingApprovals,
3616
- {
3617
- approvalId: event.approvalId,
3618
- runId: latestRunId || conversation.runtimeRunId || "",
3619
- tool: event.tool,
3620
- toolCallId: undefined,
3621
- input: (event.input ?? {}) as Record<string, unknown>,
3622
- checkpointMessages: undefined,
3623
- baseMessageCount: historyMessages.length,
3624
- pendingToolCalls: [],
3625
- },
3626
- ];
3627
- conversation.updatedAt = Date.now();
3628
- await conversationStore.update(conversation);
3629
- }
3630
- await persistDraftAssistantTurn();
3631
- }
3632
- if (event.type === "tool:approval:checkpoint") {
3633
- conversation.messages = buildMessages();
3634
- conversation.pendingApprovals = buildApprovalCheckpoints({
3635
- approvals: event.approvals,
3636
- runId: latestRunId,
3637
- checkpointMessages: event.checkpointMessages,
3638
- baseMessageCount: historyMessages.length,
3639
- pendingToolCalls: event.pendingToolCalls,
3640
- });
3641
- conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
3642
- conversation.updatedAt = Date.now();
3643
- await conversationStore.update(conversation);
3644
- checkpointedRun = true;
3645
- }
3646
- if (event.type === "run:completed") {
3647
- if (event.result.continuation && event.result.continuationMessages) {
3648
- runContinuationMessages = event.result.continuationMessages;
3649
-
3650
- conversation.messages = buildMessages();
3651
- conversation._continuationMessages = runContinuationMessages;
3652
- conversation._harnessMessages = runContinuationMessages;
3653
- conversation._toolResultArchive = harness.getToolResultArchive(conversationId);
3654
- conversation.runtimeRunId = latestRunId || conversation.runtimeRunId;
3655
- if (!checkpointedRun) {
3656
- conversation.pendingApprovals = [];
3657
- }
3658
- if ((event.result.contextTokens ?? 0) > 0) conversation.contextTokens = event.result.contextTokens!;
3659
- if ((event.result.contextWindow ?? 0) > 0) conversation.contextWindow = event.result.contextWindow!;
3660
- conversation.updatedAt = Date.now();
3661
- await conversationStore.update(conversation);
3662
-
3663
- if (!checkpointedRun) {
3664
- doWaitUntil(
3665
- new Promise(r => setTimeout(r, 3000)).then(() =>
3666
- selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
3667
- ),
3668
- );
3669
- }
3670
- }
3671
- }
3672
-
3673
3472
  await telemetry.emit(event);
3473
+ // Strip large internal payloads before sending over the wire.
3674
3474
  let sseEvent: AgentEvent = event.type === "compaction:completed" && event.compactedMessages
3675
3475
  ? { ...event, compactedMessages: undefined }
3676
3476
  : event;
3677
3477
  if (sseEvent.type === "run:completed") {
3678
3478
  const hasPendingSubagents = await hasPendingSubagentWorkForParent(conversationId, ownerId);
3679
3479
  const stripped = { ...sseEvent, result: { ...sseEvent.result, continuationMessages: undefined } };
3680
- if (hasPendingSubagents) {
3681
- sseEvent = { ...stripped, pendingSubagents: true };
3682
- } else {
3683
- sseEvent = stripped;
3684
- }
3480
+ sseEvent = hasPendingSubagents ? { ...stripped, pendingSubagents: true } : stripped;
3685
3481
  }
3686
3482
  broadcastEvent(conversationId, sseEvent);
3687
- try {
3688
- response.write(formatSseEvent(sseEvent));
3689
- } catch {
3690
- // Client disconnected — continue processing so the run completes.
3691
- }
3483
+ try { response.write(formatSseEvent(sseEvent)); } catch {}
3692
3484
  emitBrowserStatusIfActive(conversationId, event, response);
3693
3485
  },
3694
3486
  });
3695
3487
 
3696
- flushTurnDraft(draft);
3697
- latestRunId = execution.latestRunId || latestRunId;
3698
-
3699
- if (!checkpointedRun && !runContinuationMessages) {
3700
- conversation.messages = buildMessages();
3701
- applyTurnMetadata(conversation, {
3702
- latestRunId,
3703
- contextTokens: execution.runContextTokens,
3704
- contextWindow: execution.runContextWindow,
3705
- harnessMessages: execution.runHarnessMessages,
3706
- toolResultArchive: harness.getToolResultArchive(conversationId),
3707
- }, { shouldRebuildCanonical });
3708
- await conversationStore.update(conversation);
3488
+ // Trigger the auto-continuation HTTP roundtrip so the next run
3489
+ // queues. Helper has already persisted the continuation messages.
3490
+ if (result.continuation && !result.checkpointed) {
3491
+ doWaitUntil(
3492
+ new Promise(r => setTimeout(r, 3000)).then(() =>
3493
+ selfFetchWithRetry(`/api/internal/continue/${encodeURIComponent(conversationId)}`),
3494
+ ),
3495
+ );
3709
3496
  }
3710
3497
  } catch (error) {
3711
- flushTurnDraft(draft);
3712
- if (abortController.signal.aborted || runCancelled) {
3713
- if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
3714
- conversation.messages = buildMessages();
3715
- // Keep _harnessMessages aligned with what the model just saw.
3716
- // Without this, loadCanonicalHistory will hand the next turn a
3717
- // pre-cancellation snapshot and the agent will have no memory of
3718
- // the work it just did.
3719
- applyTurnMetadata(conversation, {
3720
- latestRunId,
3721
- contextTokens: 0,
3722
- contextWindow: 0,
3723
- harnessMessages: cancelHarnessMessages,
3724
- toolResultArchive: harness.getToolResultArchive(conversationId),
3725
- }, { shouldRebuildCanonical: true });
3726
- await conversationStore.update(conversation);
3727
- }
3728
- if (!checkpointedRun) {
3729
- await clearPendingApprovalsForConversation(conversationId);
3730
- }
3731
- return;
3732
- }
3498
+ // The helper handles cancel/error internally and returns gracefully.
3499
+ // This catches anything that escapes — typically a helper bug or
3500
+ // the conversation being deleted between auth check and helper reload.
3501
+ log.error(`runConversationTurn threw: ${formatError(error)}`);
3733
3502
  try {
3734
3503
  response.write(
3735
3504
  formatSseEvent({
@@ -3741,13 +3510,7 @@ export const createRequestHandler = async (options?: {
3741
3510
  },
3742
3511
  }),
3743
3512
  );
3744
- } catch {
3745
- if (draft.assistantResponse.length > 0 || draft.toolTimeline.length > 0 || draft.sections.length > 0) {
3746
- conversation.messages = buildMessages();
3747
- conversation.updatedAt = Date.now();
3748
- await conversationStore.update(conversation);
3749
- }
3750
- }
3513
+ } catch {}
3751
3514
  } finally {
3752
3515
  unsubSubagentEvents();
3753
3516
  const active = activeConversationRuns.get(conversationId);
@@ -439,7 +439,7 @@ export const WEB_UI_STYLES = `
439
439
  .sidebar-segmented {
440
440
  display: inline-flex;
441
441
  align-self: stretch;
442
- margin: 12px 6px 0;
442
+ margin: 12px 6px 8px;
443
443
  padding: 3px;
444
444
  background: var(--surface-3);
445
445
  border-radius: 999px;