@poncho-ai/sdk 1.8.1 → 1.10.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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/sdk@1.8.1 build /home/runner/work/poncho-ai/poncho-ai/packages/sdk
2
+ > @poncho-ai/sdk@1.10.0 build /home/runner/work/poncho-ai/poncho-ai/packages/sdk
3
3
  > tsup src/index.ts --format esm --dts
4
4
 
5
5
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 12.46 KB
11
- ESM ⚡️ Build success in 16ms
10
+ ESM dist/index.js 17.24 KB
11
+ ESM ⚡️ Build success in 21ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 1193ms
14
- DTS dist/index.d.ts 24.92 KB
13
+ DTS ⚡️ Build success in 1318ms
14
+ DTS dist/index.d.ts 28.29 KB
package/CHANGELOG.md CHANGED
@@ -1,5 +1,190 @@
1
1
  # @poncho-ai/sdk
2
2
 
3
+ ## 1.10.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#104](https://github.com/cesr/poncho-ai/pull/104) [`9616060`](https://github.com/cesr/poncho-ai/commit/96160607502c2c0b05bc60b67b8fc012f4052ef1) Thanks [@cesr](https://github.com/cesr)! - dev: add a Files mode to the sidebar with VFS browsing, preview, and uploads
8
+
9
+ The web UI sidebar now has a Chats / Files segmented control. Switching to
10
+ Files reveals a folder-tree view of the agent's VFS — the same storage the
11
+ agent reads and writes via `read_file`, `write_file`, and the virtualized
12
+ `bash` tool. Folders expand inline with the same caret/dropdown pattern as
13
+ the Cron jobs section. Clicking a file previews it in the main panel:
14
+ - Text / JSON / source code render as a wrapped `<pre>` (5 MB cap), with an Edit button for inline editing (last-write-wins via PUT).
15
+ - Images render inline.
16
+ - PDFs render in an embedded iframe.
17
+ - Audio and video render with native controls.
18
+ - Anything else shows a placeholder card with a Download button.
19
+
20
+ Files can be added directly from the UI: an Upload button (multi-file
21
+ picker), drag-and-drop onto the explorer or onto a specific folder row, and
22
+ a New folder button. Files and folders are deletable via a hover-X with a
23
+ two-step confirm. Conflicts prompt to overwrite. Four new HTTP routes back
24
+ this UI: `GET /api/vfs-list`, `PUT /api/vfs/{path}`, `DELETE /api/vfs/{path}`,
25
+ and `POST /api/vfs-mkdir`. URLs of the form `/f/{path}` deep-link to a file
26
+ preview; the chat composer is hidden while previewing a file.
27
+
28
+ The same routes are exposed on `AgentClient` for programmatic use:
29
+ `listDir`, `writeFile`, `deleteFile`, and `mkdir` (alongside the existing
30
+ `readFile`). New shared types `ApiVfsEntry`, `ApiVfsListResponse`, and
31
+ `ApiVfsWriteResponse` are exported from `@poncho-ai/sdk`.
32
+
33
+ ### Patch Changes
34
+
35
+ - [`524df41`](https://github.com/cesr/poncho-ai/commit/524df411904bd00c07901695eda6d4dd07dde972) Thanks [@cesr](https://github.com/cesr)! - fix: persist harness messages on cancelled runs so the agent doesn't lose context
36
+
37
+ When a run was cancelled (Stop button, abort signal), `conversation.messages`
38
+ was updated with the partial assistant turn but `conversation._harnessMessages`
39
+ — the canonical history `loadCanonicalHistory` hands to the model on the next
40
+ turn — was left holding a snapshot from the _previous_ successful run. The
41
+ agent had no memory of the cancelled work, even though the user-facing UI
42
+ still showed it. The new verbose-mode harness toggle made this divergence
43
+ directly visible.
44
+
45
+ The fix plumbs an in-flight `messages` snapshot through the `run:cancelled`
46
+ event, trims it to a model-valid prefix (no orphan `tool_use`), and persists
47
+ it as `_harnessMessages` on every cancel path in the CLI.
48
+
49
+ ## 1.9.0
50
+
51
+ ### Minor Changes
52
+
53
+ - [#100](https://github.com/cesr/poncho-ai/pull/100) [`ef4fe5d`](https://github.com/cesr/poncho-ai/commit/ef4fe5d1fd4bb31c201fd240f4524b64f01e3e6d) Thanks [@cesr](https://github.com/cesr)! - feat(logging): readable, scoped, level-aware dev server logs
54
+
55
+ `poncho dev` output is now formatted consistently across the CLI and
56
+ harness:
57
+
58
+ ```
59
+ 20:23:45 ✓ poncho dev server ready at http://localhost:3000
60
+ 20:23:45 • slack enabled at /api/messaging/slack
61
+ 20:23:45 • cron scheduled 2 jobs: hourly_check, nightly_summary
62
+ 20:24:15 → cron:hourly_check started
63
+ 20:24:17 ✓ cron:hourly_check completed in 1.2s (3 chats)
64
+ 20:25:00 ⚠ telegram approval not found: req-7f42a
65
+ 20:25:01 ✗ poncho internal error: ECONNREFUSED
66
+ ```
67
+
68
+ Format: `HH:mm:ss <symbol> <scope> <message>`. Scopes (`poncho`, `cron`,
69
+ `reminder`, `messaging`, `slack`, `telegram`, `resend`, `subagent`,
70
+ `approval`, `browser`, `csrf`, `upload`, `serverless`, `self-fetch`,
71
+ `mcp`, `telemetry`, `cost`, `model`, `harness`, `event`, `tools`)
72
+ replace the previous mix of `[poncho]`, `[poncho][cost]`, `[cron]`,
73
+ `[messaging-runner]`, `[event] ...`, etc.
74
+ - New `createLogger(scope)` exported from `@poncho-ai/sdk` with
75
+ `.debug/.info/.warn/.error/.success/.ready/.item/.child(sub)` and
76
+ helpers `formatError`, `url`, `muted`, `num`.
77
+ - Honors `NO_COLOR` / `FORCE_COLOR` and `LOG_LEVEL=debug|info|warn|error|silent`.
78
+ Verbose telemetry/cost/event lines now log at `debug` and are silent
79
+ by default.
80
+ - `poncho dev` gains `-v`/`--verbose` (debug), `-q`/`--quiet` (warn+),
81
+ and `--log-level <level>` flags.
82
+ - Each scope tag is colored with a stable pastel hue (truecolor), with
83
+ 256-color and 16-color fallbacks. Children (`cron:hourly_check`)
84
+ inherit their parent's color.
85
+ - TTY-aware: ANSI color is stripped when stdout is piped.
86
+ - Conversation-egress logging (`[poncho][egress] read: …`) is now opt-in
87
+ via `PONCHO_LOG_EGRESS=1` (matching the documented behavior; it had
88
+ been logging unconditionally).
89
+ - No behavior changes to which events are emitted — only formatting.
90
+
91
+ - [#100](https://github.com/cesr/poncho-ai/pull/100) [`ef4fe5d`](https://github.com/cesr/poncho-ai/commit/ef4fe5d1fd4bb31c201fd240f4524b64f01e3e6d) Thanks [@cesr](https://github.com/cesr)! - feat: Slack-style message threads
92
+
93
+ Users can now fork any persisted message into one or more threads. Each
94
+ thread is a new conversation whose initial history is a snapshot of the
95
+ parent up to and including the anchor message; replies in the thread
96
+ evolve independently of the parent. Multiple threads per parent message
97
+ are supported.
98
+
99
+ ## Web UI
100
+ - Hover any message in the main pane to reveal a "Reply in thread" pill
101
+ positioned just below the bubble (offset varies by role). The pill is
102
+ invisible by default and only appears on hover; a delayed-hide bridges
103
+ the empty space between message and pill so the user can move the
104
+ mouse onto it without it flickering off.
105
+ - Once a thread exists on a message, the pill is replaced by an
106
+ always-visible badge (`"N replies · 5m ago"`, count bold + meta muted).
107
+ Multiple threads stack vertically under the message, each with its own
108
+ badge. Hovering a badge reveals an outside-positioned `×` delete with
109
+ the same two-step "× → sure?" confirmation as the sidebar
110
+ conversation-delete.
111
+ - Clicking a badge opens the thread in a right-side panel that mirrors
112
+ the existing browser-panel pattern: a flex sibling of `.main-chat`
113
+ with a 1px drag-resize handle. The panel has its own composer
114
+ (independent file uploads, paste-to-attach, attachment preview)
115
+ rendered alongside the main composer so users can keep typing in the
116
+ parent conversation. Vertical padding matches the main composer so
117
+ both chatboxes line up at the same baseline.
118
+ - The pinned parent message and replies inside the panel render through
119
+ the same DOM construction logic as the main pane (assistant avatar +
120
+ markdown, user bubble + file thumbnails). Reply submissions stream
121
+ token-by-token via SSE (parsed model:chunk events feed an optimistic
122
+ assistant placeholder; a thinking-indicator shows until the first
123
+ chunk lands).
124
+ - The open thread is reflected in the URL hash (`#thread=<id>`) so a
125
+ page reload restores the panel. Switching conversations or closing
126
+ the panel clears the hash and any sticky drag-resize widths.
127
+
128
+ ## DB
129
+ - New `parent_message_id TEXT` column on `conversations` (migration 6)
130
+ plus a partial index on `(parent_conversation_id, parent_message_id)
131
+ WHERE parent_message_id IS NOT NULL`.
132
+ - The existing `parent_conversation_id` plumbing is reused; subagents
133
+ and threads coexist on that column, discriminated by whether
134
+ `parent_message_id` is set (subagents leave it `NULL`).
135
+ - `threadMeta` (snapshot length + cached parent-message summary)
136
+ round-trips inside the conversation `data` blob.
137
+
138
+ ## API
139
+ - `GET /api/conversations/:id/threads` → `{ threads: ApiThreadSummary[] }`
140
+ - `POST /api/conversations/:id/threads { parentMessageId, title? }` →
141
+ 201 `{ thread, conversationId }` |
142
+ 404 `PARENT_MESSAGE_NOT_FOUND` |
143
+ 409 `MESSAGE_ID_REQUIRED` (anchor lacks a stable id) |
144
+ 409 `ANCHOR_IN_FLIGHT` (anchor is the streaming tail of a live run)
145
+ - The two `SUBAGENT_READ_ONLY` gates on `/messages` and `/continue` are
146
+ now keyed on `subagentMeta` rather than `parentConversationId` so
147
+ threads remain writable.
148
+ - New `ApiThreadSummary`, `ApiThreadListResponse`,
149
+ `ApiCreateThreadRequest`, `ApiCreateThreadResponse` types in
150
+ `@poncho-ai/sdk`. New `AgentClient.listThreads` /
151
+ `AgentClient.createThread` wrappers in `@poncho-ai/client`.
152
+
153
+ ## Storage interface
154
+ - `ConversationStore.listThreads(parentConversationId)` and the
155
+ matching `StorageEngine.conversations.listThreads(...)`. External
156
+ implementers of these interfaces will need to add the method.
157
+ - `Conversation` / `ConversationCreateInit` / `ConversationSummary`
158
+ gained optional `parentMessageId` and `threadMeta` fields.
159
+
160
+ ## Fork semantics
161
+ - Stable `metadata.id` on every persisted message: `randomUUID()` is
162
+ hoisted once per turn and reused for both the user message and the
163
+ in-flight assistant message across all persist sites (cli messaging
164
+ run, cli `/messages` handler, cron path). `buildAssistantMetadata`
165
+ takes an optional `{ id, timestamp }` opt-arg.
166
+ - No DB backfill of legacy id-less messages; the SPA hides the
167
+ "Reply in thread" affordance on rows whose `metadata.id` is missing.
168
+ - The visible-sequence used for the anchor lookup is reconstructed as
169
+ `[...compactedHistory, ...messages.filter(notCompactionSummary)]`,
170
+ so pre-compaction anchors are supported. For pre-compaction anchors,
171
+ `_harnessMessages` is reset to `undefined` so the harness rebuilds
172
+ canonical history from `messages` on the thread's first run.
173
+ - Forking on the actively-streaming tail message of a live run returns
174
+ 409 `ANCHOR_IN_FLIGHT`; any prior, already-persisted message is
175
+ fork-able even while the parent is mid-run.
176
+ - Tool-result archive entries are filtered to only those referenced by
177
+ tool calls in the trimmed `_harnessMessages` (no whole-archive clones).
178
+ - All run-specific state is reset on the new thread:
179
+ `runtimeRunId`, `pendingApprovals`, `runStatus`,
180
+ `pendingSubagentResults`, `subagentCallbackCount`,
181
+ `runningCallbackSince`, `_continuationMessages`. `channelMeta` and
182
+ `subagentMeta` are explicitly NOT inherited so threads aren't bound
183
+ to the parent's Slack/Telegram thread and aren't subagent runs.
184
+ - Thread conversations stay out of the sidebar list (already-existing
185
+ `!c.parentConversationId` filter) and are cascade-deleted with their
186
+ parent.
187
+
3
188
  ## 1.8.1
4
189
 
5
190
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -528,6 +528,120 @@ declare const ONBOARDING_FIELDS: readonly [{
528
528
  declare const FEATURE_DOMAIN_ORDER: readonly FeatureDomain[];
529
529
  declare const fieldsForScope: (scope: OnboardingScope) => OnboardingField[];
530
530
 
531
+ /**
532
+ * Shared API response types used by both the CLI server and the client SDK.
533
+ * Defining them here ensures compile-time drift detection between the two.
534
+ */
535
+ interface ApiApprovalResponse {
536
+ ok: true;
537
+ approvalId: string;
538
+ approved: boolean;
539
+ batchComplete?: boolean;
540
+ }
541
+ interface ApiStopRunResponse {
542
+ ok: true;
543
+ stopped: boolean;
544
+ runId?: string;
545
+ }
546
+ interface ApiCompactResponse {
547
+ compacted: boolean;
548
+ messagesBefore: number;
549
+ messagesAfter: number;
550
+ warning?: string;
551
+ }
552
+ interface ApiSubagentSummary {
553
+ conversationId: string;
554
+ title: string;
555
+ task: string;
556
+ status: string;
557
+ messageCount: number;
558
+ hasPendingApprovals: boolean;
559
+ createdAt: string;
560
+ updatedAt: string;
561
+ }
562
+ interface ApiThreadSummary {
563
+ conversationId: string;
564
+ parentConversationId: string;
565
+ parentMessageId: string;
566
+ title: string;
567
+ parentMessageSummary?: string;
568
+ messageCount: number;
569
+ /** messageCount - snapshotLength: number of replies posted into the thread. */
570
+ replyCount: number;
571
+ snapshotLength: number;
572
+ createdAt: number;
573
+ updatedAt: number;
574
+ /** Same as updatedAt; named for the inline-row UI. */
575
+ lastReplyAt: number;
576
+ }
577
+ interface ApiThreadListResponse {
578
+ threads: ApiThreadSummary[];
579
+ }
580
+ interface ApiCreateThreadRequest {
581
+ parentMessageId: string;
582
+ title?: string;
583
+ }
584
+ interface ApiCreateThreadResponse {
585
+ thread: ApiThreadSummary;
586
+ conversationId: string;
587
+ }
588
+ interface ApiSecretEntry {
589
+ name: string;
590
+ label?: string;
591
+ isSet: boolean;
592
+ }
593
+ interface ApiSlashCommand {
594
+ command: string;
595
+ description: string;
596
+ type: "command" | "skill";
597
+ }
598
+ interface ApiVfsEntry {
599
+ name: string;
600
+ type: "file" | "directory" | "symlink";
601
+ size: number;
602
+ mimeType: string | null;
603
+ updatedAt: number | null;
604
+ }
605
+ interface ApiVfsListResponse {
606
+ path: string;
607
+ entries: ApiVfsEntry[];
608
+ usage: {
609
+ fileCount: number;
610
+ totalBytes: number;
611
+ };
612
+ }
613
+ interface ApiVfsWriteResponse {
614
+ path: string;
615
+ size: number;
616
+ mimeType: string | null;
617
+ updatedAt: number;
618
+ }
619
+
620
+ declare const LEVELS: {
621
+ readonly debug: 10;
622
+ readonly info: 20;
623
+ readonly warn: 30;
624
+ readonly error: 40;
625
+ readonly silent: 100;
626
+ };
627
+ type LevelName = keyof typeof LEVELS;
628
+ declare const setLogLevel: (level: LevelName) => void;
629
+ type Logger = {
630
+ debug: (msg: string, ...extras: unknown[]) => void;
631
+ info: (msg: string, ...extras: unknown[]) => void;
632
+ warn: (msg: string, ...extras: unknown[]) => void;
633
+ error: (msg: string, ...extras: unknown[]) => void;
634
+ success: (msg: string, ...extras: unknown[]) => void;
635
+ ready: (msg: string, ...extras: unknown[]) => void;
636
+ item: (msg: string, ...extras: unknown[]) => void;
637
+ child: (subscope: string) => Logger;
638
+ };
639
+ declare const createLogger: (scope: string) => Logger;
640
+ declare const formatError: (err: unknown) => string;
641
+ declare const url: (s: string) => string;
642
+ declare const muted: (s: string) => string;
643
+ declare const num: (s: string | number) => string;
644
+
531
645
  type JsonSchema = {
532
646
  type?: string;
533
647
  description?: string;
@@ -677,6 +791,7 @@ type AgentEvent = {
677
791
  } | {
678
792
  type: "run:cancelled";
679
793
  runId: string;
794
+ messages?: Message[];
680
795
  } | {
681
796
  type: "run:error";
682
797
  runId: string;
@@ -795,4 +910,4 @@ type AgentEvent = {
795
910
  reason: string;
796
911
  };
797
912
 
798
- export { type AgentEvent, type AgentFailure, type ContentPart, FEATURE_DOMAIN_ORDER, type FeatureDomain, type FileContentPart, type FileInput, type JsonSchema, type Message, ONBOARDING_FIELDS, type OnboardingField, type OnboardingFieldCondition, type OnboardingFieldKind, type OnboardingFieldTarget, type OnboardingOption, type OnboardingScope, type Role, type RunInput, type RunResult, type TextContentPart, type TokenUsage, type ToolContext, type ToolDefinition, type ToolHandler, type VfsAccess, defineTool, fieldsForScope, getTextContent };
913
+ export { type AgentEvent, type AgentFailure, type ApiApprovalResponse, type ApiCompactResponse, type ApiCreateThreadRequest, type ApiCreateThreadResponse, type ApiSecretEntry, type ApiSlashCommand, type ApiStopRunResponse, type ApiSubagentSummary, type ApiThreadListResponse, type ApiThreadSummary, type ApiVfsEntry, type ApiVfsListResponse, type ApiVfsWriteResponse, type ContentPart, FEATURE_DOMAIN_ORDER, type FeatureDomain, type FileContentPart, type FileInput, type JsonSchema, type Logger, type Message, ONBOARDING_FIELDS, type OnboardingField, type OnboardingFieldCondition, type OnboardingFieldKind, type OnboardingFieldTarget, type OnboardingOption, type OnboardingScope, type Role, type RunInput, type RunResult, type TextContentPart, type TokenUsage, type ToolContext, type ToolDefinition, type ToolHandler, type VfsAccess, createLogger, defineTool, fieldsForScope, formatError, getTextContent, muted, num, setLogLevel, url };
package/dist/index.js CHANGED
@@ -439,6 +439,189 @@ var fieldsForScope = (scope) => ONBOARDING_FIELDS.filter(
439
439
  (field) => field.scopes.includes(scope)
440
440
  );
441
441
 
442
+ // src/logger.ts
443
+ var LEVELS = { debug: 10, info: 20, warn: 30, error: 40, silent: 100 };
444
+ var parseLevel = (raw) => {
445
+ const v = (raw ?? "").trim().toLowerCase();
446
+ return v in LEVELS ? v : "info";
447
+ };
448
+ var colorEnabled = (() => {
449
+ if (process.env.NO_COLOR) return false;
450
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
451
+ return Boolean(process.stdout.isTTY);
452
+ })();
453
+ var wrap = (open, close = 0) => colorEnabled ? (s) => `\x1B[${open}m${s}\x1B[${close}m` : (s) => s;
454
+ var c = {
455
+ dim: wrap(2, 22),
456
+ bold: wrap(1, 22),
457
+ red: wrap(31),
458
+ green: wrap(32),
459
+ yellow: wrap(33),
460
+ blue: wrap(34),
461
+ magenta: wrap(35),
462
+ cyan: wrap(36),
463
+ gray: wrap(90)
464
+ };
465
+ var supportsTruecolor = (() => {
466
+ if (!colorEnabled) return false;
467
+ const t = process.env.COLORTERM;
468
+ return t === "truecolor" || t === "24bit";
469
+ })();
470
+ var supports256 = (() => {
471
+ if (!colorEnabled) return false;
472
+ if (supportsTruecolor) return true;
473
+ return Boolean(process.env.TERM && /256/.test(process.env.TERM));
474
+ })();
475
+ var PALETTE_256 = [
476
+ 33,
477
+ 39,
478
+ 38,
479
+ 75,
480
+ 81,
481
+ 45,
482
+ 50,
483
+ 49,
484
+ 43,
485
+ 36,
486
+ 73,
487
+ 80,
488
+ 86,
489
+ 76,
490
+ 82,
491
+ 113,
492
+ 119,
493
+ 148,
494
+ 149,
495
+ 178,
496
+ 184,
497
+ 220,
498
+ 221,
499
+ 208,
500
+ 209,
501
+ 214,
502
+ 215,
503
+ 202,
504
+ 203,
505
+ 198,
506
+ 199,
507
+ 200,
508
+ 207,
509
+ 206,
510
+ 171,
511
+ 165,
512
+ 135,
513
+ 141,
514
+ 99,
515
+ 105,
516
+ 129,
517
+ 134,
518
+ 174
519
+ ];
520
+ var PALETTE_16 = [31, 32, 33, 34, 35, 36, 91, 92, 93, 94, 95, 96];
521
+ var hashScope = (s) => {
522
+ let h = 5381;
523
+ for (let i = 0; i < s.length; i++) h = (h * 33 ^ s.charCodeAt(i)) >>> 0;
524
+ return h;
525
+ };
526
+ var hslToRgb = (h, s, l) => {
527
+ const a = s * Math.min(l, 1 - l);
528
+ const f = (n) => {
529
+ const k = (n + h / 30) % 12;
530
+ return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
531
+ };
532
+ return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
533
+ };
534
+ var scopeColorCache = /* @__PURE__ */ new Map();
535
+ var colorForScope = (scope) => {
536
+ if (!colorEnabled) return (s) => s;
537
+ const base = scope.split(":")[0];
538
+ const cached = scopeColorCache.get(base);
539
+ if (cached) return cached;
540
+ const hash = hashScope(base);
541
+ let fn;
542
+ if (supportsTruecolor) {
543
+ const hue = hash * 137 % 360;
544
+ const [r, g, b] = hslToRgb(hue, 0.4, 0.75);
545
+ fn = (s) => `\x1B[38;2;${r};${g};${b}m${s}\x1B[39m`;
546
+ } else if (supports256) {
547
+ const code = PALETTE_256[hash % PALETTE_256.length];
548
+ fn = (s) => `\x1B[38;5;${code}m${s}\x1B[39m`;
549
+ } else {
550
+ const code = PALETTE_16[hash % PALETTE_16.length];
551
+ fn = (s) => `\x1B[${code}m${s}\x1B[39m`;
552
+ }
553
+ scopeColorCache.set(base, fn);
554
+ return fn;
555
+ };
556
+ var pad2 = (n) => n < 10 ? `0${n}` : String(n);
557
+ var formatTime = (d) => `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
558
+ var SCOPE_WIDTH = 10;
559
+ var padScope = (scope) => scope.length >= SCOPE_WIDTH ? scope : scope + " ".repeat(SCOPE_WIDTH - scope.length);
560
+ var SYMBOLS = {
561
+ debug: "\xB7",
562
+ info: "\u2192",
563
+ warn: "\u26A0",
564
+ error: "\u2717",
565
+ success: "\u2713",
566
+ ready: "\u2713",
567
+ neutral: "\u2022"
568
+ };
569
+ var SYMBOL_COLOR = {
570
+ debug: c.dim,
571
+ info: c.cyan,
572
+ warn: c.yellow,
573
+ error: c.red,
574
+ success: c.green,
575
+ ready: c.green,
576
+ neutral: c.gray
577
+ };
578
+ var VARIANT_LEVEL = {
579
+ debug: "debug",
580
+ info: "info",
581
+ neutral: "info",
582
+ success: "info",
583
+ ready: "info",
584
+ warn: "warn",
585
+ error: "error"
586
+ };
587
+ var currentLevel = parseLevel(process.env.LOG_LEVEL);
588
+ var setLogLevel = (level) => {
589
+ currentLevel = level;
590
+ };
591
+ var stringifyArg = (a) => {
592
+ if (a instanceof Error) return a.stack ?? a.message;
593
+ if (typeof a === "string") return a;
594
+ try {
595
+ return JSON.stringify(a);
596
+ } catch {
597
+ return String(a);
598
+ }
599
+ };
600
+ var write = (variant, scope, msg, extras) => {
601
+ if (LEVELS[VARIANT_LEVEL[variant]] < LEVELS[currentLevel]) return;
602
+ const stream = variant === "warn" || variant === "error" ? process.stderr : process.stdout;
603
+ const time = c.gray(formatTime(/* @__PURE__ */ new Date()));
604
+ const symbol = SYMBOL_COLOR[variant](SYMBOLS[variant]);
605
+ const scopeText = colorForScope(scope)(padScope(scope));
606
+ const tail = extras.length > 0 ? " " + extras.map(stringifyArg).join(" ") : "";
607
+ stream.write(`${time} ${symbol} ${scopeText} ${msg}${tail}
608
+ `);
609
+ };
610
+ var createLogger = (scope) => ({
611
+ debug: (msg, ...extras) => write("debug", scope, msg, extras),
612
+ info: (msg, ...extras) => write("info", scope, msg, extras),
613
+ warn: (msg, ...extras) => write("warn", scope, msg, extras),
614
+ error: (msg, ...extras) => write("error", scope, msg, extras),
615
+ success: (msg, ...extras) => write("success", scope, msg, extras),
616
+ ready: (msg, ...extras) => write("ready", scope, msg, extras),
617
+ item: (msg, ...extras) => write("neutral", scope, msg, extras),
618
+ child: (subscope) => createLogger(`${scope}:${subscope}`)
619
+ });
620
+ var formatError = (err) => err instanceof Error ? err.message : String(err);
621
+ var url = (s) => c.cyan(s);
622
+ var muted = (s) => c.dim(s);
623
+ var num = (s) => c.bold(String(s));
624
+
442
625
  // src/index.ts
443
626
  var getTextContent = (message) => {
444
627
  if (typeof message.content === "string") return message.content;
@@ -448,7 +631,13 @@ var defineTool = (definition) => definition;
448
631
  export {
449
632
  FEATURE_DOMAIN_ORDER,
450
633
  ONBOARDING_FIELDS,
634
+ createLogger,
451
635
  defineTool,
452
636
  fieldsForScope,
453
- getTextContent
637
+ formatError,
638
+ getTextContent,
639
+ muted,
640
+ num,
641
+ setLogLevel,
642
+ url
454
643
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/sdk",
3
- "version": "1.8.1",
3
+ "version": "1.10.0",
4
4
  "description": "Core types and utilities for building Poncho skills",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Shared API response types used by both the CLI server and the client SDK.
3
+ * Defining them here ensures compile-time drift detection between the two.
4
+ */
5
+
6
+ export interface ApiApprovalResponse {
7
+ ok: true;
8
+ approvalId: string;
9
+ approved: boolean;
10
+ batchComplete?: boolean;
11
+ }
12
+
13
+ export interface ApiStopRunResponse {
14
+ ok: true;
15
+ stopped: boolean;
16
+ runId?: string;
17
+ }
18
+
19
+ export interface ApiCompactResponse {
20
+ compacted: boolean;
21
+ messagesBefore: number;
22
+ messagesAfter: number;
23
+ warning?: string;
24
+ }
25
+
26
+ export interface ApiSubagentSummary {
27
+ conversationId: string;
28
+ title: string;
29
+ task: string;
30
+ status: string;
31
+ messageCount: number;
32
+ hasPendingApprovals: boolean;
33
+ createdAt: string;
34
+ updatedAt: string;
35
+ }
36
+
37
+ export interface ApiThreadSummary {
38
+ conversationId: string;
39
+ parentConversationId: string;
40
+ parentMessageId: string;
41
+ title: string;
42
+ parentMessageSummary?: string;
43
+ messageCount: number;
44
+ /** messageCount - snapshotLength: number of replies posted into the thread. */
45
+ replyCount: number;
46
+ snapshotLength: number;
47
+ createdAt: number;
48
+ updatedAt: number;
49
+ /** Same as updatedAt; named for the inline-row UI. */
50
+ lastReplyAt: number;
51
+ }
52
+
53
+ export interface ApiThreadListResponse {
54
+ threads: ApiThreadSummary[];
55
+ }
56
+
57
+ export interface ApiCreateThreadRequest {
58
+ parentMessageId: string;
59
+ title?: string;
60
+ }
61
+
62
+ export interface ApiCreateThreadResponse {
63
+ thread: ApiThreadSummary;
64
+ conversationId: string;
65
+ }
66
+
67
+ export interface ApiSecretEntry {
68
+ name: string;
69
+ label?: string;
70
+ isSet: boolean;
71
+ }
72
+
73
+ export interface ApiSlashCommand {
74
+ command: string;
75
+ description: string;
76
+ type: "command" | "skill";
77
+ }
78
+
79
+ export interface ApiVfsEntry {
80
+ name: string;
81
+ type: "file" | "directory" | "symlink";
82
+ size: number;
83
+ mimeType: string | null;
84
+ updatedAt: number | null;
85
+ }
86
+
87
+ export interface ApiVfsListResponse {
88
+ path: string;
89
+ entries: ApiVfsEntry[];
90
+ usage: { fileCount: number; totalBytes: number };
91
+ }
92
+
93
+ export interface ApiVfsWriteResponse {
94
+ path: string;
95
+ size: number;
96
+ mimeType: string | null;
97
+ updatedAt: number;
98
+ }
package/src/index.ts CHANGED
@@ -109,6 +109,7 @@ export const defineTool = <
109
109
  ): ToolDefinition<TInput, TOutput> => definition;
110
110
 
111
111
  export * from "./config-registry.js";
112
+ export * from "./api-types.js";
112
113
 
113
114
  export interface FileInput {
114
115
  /** base64 data, data: URI, or https:// URL */
@@ -165,7 +166,7 @@ export interface AgentFailure {
165
166
  export type AgentEvent =
166
167
  | { type: "run:started"; runId: string; agentId: string; contextWindow?: number }
167
168
  | { type: "run:completed"; runId: string; result: RunResult; pendingSubagents?: boolean }
168
- | { type: "run:cancelled"; runId: string }
169
+ | { type: "run:cancelled"; runId: string; messages?: Message[] }
169
170
  | { type: "run:error"; runId: string; error: AgentFailure }
170
171
  | { type: "step:started"; step: number }
171
172
  | { type: "step:completed"; step: number; duration: number }
@@ -225,3 +226,13 @@ export type AgentEvent =
225
226
  }
226
227
  | { type: "subagents:pending" }
227
228
  | { type: "compaction:warning"; reason: string };
229
+
230
+ export {
231
+ createLogger,
232
+ setLogLevel,
233
+ formatError,
234
+ url,
235
+ muted,
236
+ num,
237
+ type Logger,
238
+ } from "./logger.js";
package/src/logger.ts ADDED
@@ -0,0 +1,199 @@
1
+ // Tiny zero-dependency logger for the dev server.
2
+ //
3
+ // Format: `HH:mm:ss <symbol> <scope-padded> <message>`
4
+ // 10:23:45 ✓ poncho dev server ready at http://localhost:3000
5
+ // 10:25:01 ✗ poncho internal error: ...
6
+ //
7
+ // Honors `NO_COLOR` / `FORCE_COLOR` and `LOG_LEVEL` (debug|info|warn|error|silent).
8
+
9
+ const LEVELS = { debug: 10, info: 20, warn: 30, error: 40, silent: 100 } as const;
10
+ type LevelName = keyof typeof LEVELS;
11
+
12
+ const parseLevel = (raw: string | undefined): LevelName => {
13
+ const v = (raw ?? "").trim().toLowerCase();
14
+ return v in LEVELS ? (v as LevelName) : "info";
15
+ };
16
+
17
+ const colorEnabled = ((): boolean => {
18
+ if (process.env.NO_COLOR) return false;
19
+ if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") return true;
20
+ return Boolean(process.stdout.isTTY);
21
+ })();
22
+
23
+ const wrap = (open: number, close = 0): ((s: string) => string) =>
24
+ colorEnabled
25
+ ? (s: string) => `\x1b[${open}m${s}\x1b[${close}m`
26
+ : (s: string) => s;
27
+
28
+ const c = {
29
+ dim: wrap(2, 22),
30
+ bold: wrap(1, 22),
31
+ red: wrap(31),
32
+ green: wrap(32),
33
+ yellow: wrap(33),
34
+ blue: wrap(34),
35
+ magenta: wrap(35),
36
+ cyan: wrap(36),
37
+ gray: wrap(90),
38
+ };
39
+
40
+ // Per-scope colors. Each scope name gets a stable, distinct color derived
41
+ // from a hash of its base name (so children like `cron:hourly_check` share
42
+ // the parent's color). Truecolor → unique hue per scope (best). 256-color
43
+ // fallback → curated palette. 16-color fallback → small palette.
44
+ const supportsTruecolor = ((): boolean => {
45
+ if (!colorEnabled) return false;
46
+ const t = process.env.COLORTERM;
47
+ return t === "truecolor" || t === "24bit";
48
+ })();
49
+
50
+ const supports256 = ((): boolean => {
51
+ if (!colorEnabled) return false;
52
+ if (supportsTruecolor) return true;
53
+ return Boolean(process.env.TERM && /256/.test(process.env.TERM));
54
+ })();
55
+
56
+ const PALETTE_256 = [
57
+ 33, 39, 38, 75, 81, 45, 50, 49, 43, 36, 73, 80, 86, 76, 82, 113,
58
+ 119, 148, 149, 178, 184, 220, 221, 208, 209, 214, 215, 202, 203,
59
+ 198, 199, 200, 207, 206, 171, 165, 135, 141, 99, 105, 129, 134, 174,
60
+ ];
61
+ const PALETTE_16 = [31, 32, 33, 34, 35, 36, 91, 92, 93, 94, 95, 96];
62
+
63
+ const hashScope = (s: string): number => {
64
+ let h = 5381;
65
+ for (let i = 0; i < s.length; i++) h = ((h * 33) ^ s.charCodeAt(i)) >>> 0;
66
+ return h;
67
+ };
68
+
69
+ const hslToRgb = (h: number, s: number, l: number): [number, number, number] => {
70
+ const a = s * Math.min(l, 1 - l);
71
+ const f = (n: number): number => {
72
+ const k = (n + h / 30) % 12;
73
+ return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1));
74
+ };
75
+ return [Math.round(f(0) * 255), Math.round(f(8) * 255), Math.round(f(4) * 255)];
76
+ };
77
+
78
+ const scopeColorCache = new Map<string, (s: string) => string>();
79
+ const colorForScope = (scope: string): ((s: string) => string) => {
80
+ if (!colorEnabled) return (s) => s;
81
+ const base = scope.split(":")[0]!;
82
+ const cached = scopeColorCache.get(base);
83
+ if (cached) return cached;
84
+ const hash = hashScope(base);
85
+ let fn: (s: string) => string;
86
+ if (supportsTruecolor) {
87
+ // Spread hues using the golden-angle multiplier for good visual distance
88
+ // between consecutive scopes. Mid-range S/L reads on dark and light bgs.
89
+ const hue = (hash * 137) % 360;
90
+ // Soft pastel: low saturation, high lightness — readable on dark and
91
+ // light backgrounds without being eye-grabbing.
92
+ const [r, g, b] = hslToRgb(hue, 0.40, 0.75);
93
+ fn = (s: string) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
94
+ } else if (supports256) {
95
+ const code = PALETTE_256[hash % PALETTE_256.length];
96
+ fn = (s: string) => `\x1b[38;5;${code}m${s}\x1b[39m`;
97
+ } else {
98
+ const code = PALETTE_16[hash % PALETTE_16.length];
99
+ fn = (s: string) => `\x1b[${code}m${s}\x1b[39m`;
100
+ }
101
+ scopeColorCache.set(base, fn);
102
+ return fn;
103
+ };
104
+
105
+ const pad2 = (n: number): string => (n < 10 ? `0${n}` : String(n));
106
+ const formatTime = (d: Date): string =>
107
+ `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
108
+
109
+ const SCOPE_WIDTH = 10;
110
+ const padScope = (scope: string): string =>
111
+ scope.length >= SCOPE_WIDTH ? scope : scope + " ".repeat(SCOPE_WIDTH - scope.length);
112
+
113
+ type Variant = "debug" | "info" | "warn" | "error" | "success" | "ready" | "neutral";
114
+
115
+ const SYMBOLS: Record<Variant, string> = {
116
+ debug: "·",
117
+ info: "→",
118
+ warn: "⚠",
119
+ error: "✗",
120
+ success: "✓",
121
+ ready: "✓",
122
+ neutral: "•",
123
+ };
124
+
125
+ const SYMBOL_COLOR: Record<Variant, (s: string) => string> = {
126
+ debug: c.dim,
127
+ info: c.cyan,
128
+ warn: c.yellow,
129
+ error: c.red,
130
+ success: c.green,
131
+ ready: c.green,
132
+ neutral: c.gray,
133
+ };
134
+
135
+ const VARIANT_LEVEL: Record<Variant, LevelName> = {
136
+ debug: "debug",
137
+ info: "info",
138
+ neutral: "info",
139
+ success: "info",
140
+ ready: "info",
141
+ warn: "warn",
142
+ error: "error",
143
+ };
144
+
145
+ let currentLevel: LevelName = parseLevel(process.env.LOG_LEVEL);
146
+
147
+ export const setLogLevel = (level: LevelName): void => {
148
+ currentLevel = level;
149
+ };
150
+
151
+ const stringifyArg = (a: unknown): string => {
152
+ if (a instanceof Error) return a.stack ?? a.message;
153
+ if (typeof a === "string") return a;
154
+ try {
155
+ return JSON.stringify(a);
156
+ } catch {
157
+ return String(a);
158
+ }
159
+ };
160
+
161
+ const write = (variant: Variant, scope: string, msg: string, extras: unknown[]): void => {
162
+ if (LEVELS[VARIANT_LEVEL[variant]] < LEVELS[currentLevel]) return;
163
+ const stream = variant === "warn" || variant === "error" ? process.stderr : process.stdout;
164
+ const time = c.gray(formatTime(new Date()));
165
+ const symbol = SYMBOL_COLOR[variant](SYMBOLS[variant]);
166
+ const scopeText = colorForScope(scope)(padScope(scope));
167
+ const tail = extras.length > 0 ? " " + extras.map(stringifyArg).join(" ") : "";
168
+ stream.write(`${time} ${symbol} ${scopeText} ${msg}${tail}\n`);
169
+ };
170
+
171
+ export type Logger = {
172
+ debug: (msg: string, ...extras: unknown[]) => void;
173
+ info: (msg: string, ...extras: unknown[]) => void;
174
+ warn: (msg: string, ...extras: unknown[]) => void;
175
+ error: (msg: string, ...extras: unknown[]) => void;
176
+ success: (msg: string, ...extras: unknown[]) => void;
177
+ ready: (msg: string, ...extras: unknown[]) => void;
178
+ item: (msg: string, ...extras: unknown[]) => void;
179
+ child: (subscope: string) => Logger;
180
+ };
181
+
182
+ export const createLogger = (scope: string): Logger => ({
183
+ debug: (msg, ...extras) => write("debug", scope, msg, extras),
184
+ info: (msg, ...extras) => write("info", scope, msg, extras),
185
+ warn: (msg, ...extras) => write("warn", scope, msg, extras),
186
+ error: (msg, ...extras) => write("error", scope, msg, extras),
187
+ success: (msg, ...extras) => write("success", scope, msg, extras),
188
+ ready: (msg, ...extras) => write("ready", scope, msg, extras),
189
+ item: (msg, ...extras) => write("neutral", scope, msg, extras),
190
+ child: (subscope) => createLogger(`${scope}:${subscope}`),
191
+ });
192
+
193
+ export const formatError = (err: unknown): string =>
194
+ err instanceof Error ? err.message : String(err);
195
+
196
+ // Highlight helpers — use sparingly inside messages
197
+ export const url = (s: string): string => c.cyan(s);
198
+ export const muted = (s: string): string => c.dim(s);
199
+ export const num = (s: string | number): string => c.bold(String(s));