@crouton-kit/crouter 0.3.57 → 0.3.59

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.
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Shared wire types for the crouter web client/server split.
3
+ *
4
+ * The SPA imports the pi payload types it needs from this module — never from
5
+ * broker-protocol internals. The type-only re-exports below keep the web
6
+ * package's shared payload shapes in one place.
7
+ */
8
+ export type { UserMessage, AssistantMessage, ToolResultMessage, TextContent, ToolCall, ImageContent, } from '@earendil-works/pi-ai';
9
+ export type { ThinkingLevel } from '@earendil-works/pi-agent-core';
10
+ export type { RpcExtensionUIRequest, RpcExtensionUIResponse } from '@earendil-works/pi-coding-agent';
11
+ import type { AgentMessage } from '@earendil-works/pi-agent-core';
12
+ import type { ThinkingLevel } from '@earendil-works/pi-agent-core';
13
+ /**
14
+ * An `AgentMessage` enriched with a client-side origin tag.
15
+ * Only `role:'user'` messages can carry `origin:'inbox'`; all others leave
16
+ * `origin` undefined. `'human'` is reserved for future explicit human-origin
17
+ * tagging (currently unset — absence means human by default).
18
+ *
19
+ * Defined as an intersection (not `interface extends`) because `AgentMessage`
20
+ * is a union type and TypeScript does not allow extending unions.
21
+ */
22
+ export type FoldedMessage = AgentMessage & {
23
+ /** Set to `'inbox'` when this user message is recognized as injected by the
24
+ * canvas-inbox-watcher extension (a coalesced inbox digest). */
25
+ origin?: 'inbox' | 'human';
26
+ };
27
+ /** base session vs orchestrator. */
28
+ export type NodeMode = 'base' | 'orchestrator';
29
+ /** terminal (reaps when done) vs resident (root) node. */
30
+ export type NodeLifecycle = 'terminal' | 'resident';
31
+ /** Canvas lifecycle status. */
32
+ export type NodeLifeStatus = 'active' | 'idle' | 'done' | 'dead' | 'canceled';
33
+ /** broker-hosted (enterable) vs tmux-pane (shown, non-enterable). */
34
+ export type HostKind = 'broker' | 'tmux';
35
+ /** One row from `crtr canvas snapshot --json` — node identity + runtime, no chrome. */
36
+ export interface NodeSummary {
37
+ node_id: string;
38
+ name: string;
39
+ /** Node kind (developer/explore/…) — open set. */
40
+ kind: string;
41
+ mode: NodeMode;
42
+ lifecycle: NodeLifecycle;
43
+ status: NodeLifeStatus;
44
+ cwd: string;
45
+ /** Spine parent node id, or null for a root. */
46
+ parent: string | null;
47
+ /** ISO-8601 creation timestamp. */
48
+ created: string;
49
+ host_kind: HostKind;
50
+ /** True iff `host_kind === 'broker'` (the web UI can open a live session). */
51
+ enterable: boolean;
52
+ /** Count of pending human asks (blocked-on-human indicator). */
53
+ attention_count: number;
54
+ /** Canvas cycle count for this node (revive/yield generations). Optional for
55
+ * back-compat with snapshots produced before the field existed. */
56
+ cycles?: number;
57
+ /** ISO-8601 of the node's most recent work (session activity), distinct from
58
+ * `created`. Optional/back-compat; falls back to `created` when absent. */
59
+ last_activity?: string;
60
+ }
61
+ /** input/output token burn, with cache reads where the provider reports them. */
62
+ export interface TokenBurn {
63
+ input: number;
64
+ output: number;
65
+ cache?: number;
66
+ }
67
+ /** Live context-window usage. */
68
+ export interface ContextUsage {
69
+ tokens: number;
70
+ window: number;
71
+ percent: number;
72
+ }
73
+ /** Coarse session stats for chrome (distinct from pi's full `SessionStats`). */
74
+ export interface SessionStatsSummary {
75
+ turns: number;
76
+ user_messages: number;
77
+ assistant_messages: number;
78
+ cost?: number;
79
+ }
80
+ /** Viewer/controller presence for a node. */
81
+ export interface Presence {
82
+ viewers: number;
83
+ controller: string | null;
84
+ }
85
+ /** Git working-tree change counts for the meta strip. */
86
+ export interface GitStatus {
87
+ added: number;
88
+ modified: number;
89
+ deleted: number;
90
+ untracked: number;
91
+ }
92
+ /** `crtr node inspect show --json`'s node payload — `NodeSummary` plus the per-node chrome. */
93
+ export interface NodeDetail extends NodeSummary {
94
+ branch: string | null;
95
+ model: string | null;
96
+ tokens: TokenBurn | null;
97
+ context: ContextUsage | null;
98
+ tool_calls: number | null;
99
+ stats: SessionStatsSummary | null;
100
+ presence: Presence | null;
101
+ /** True while a broker is live; false for a dormant (last-known) view. */
102
+ live: boolean;
103
+ /** Working-tree change counts for the meta strip (null when unavailable). */
104
+ git_status?: GitStatus | null;
105
+ }
106
+ /** A slash command in a node's palette (from the broker `get_commands` reply). */
107
+ export interface Command {
108
+ name: string;
109
+ description: string;
110
+ source: 'builtin' | 'command' | 'template';
111
+ location?: 'user' | 'project' | 'path';
112
+ path?: string;
113
+ argument_hint?: string;
114
+ }
115
+ /** `crtr canvas snapshot --json` body. */
116
+ export interface CanvasSnapshot {
117
+ nodes: NodeSummary[];
118
+ /** ISO-8601. */
119
+ generated_at: string;
120
+ }
121
+ /** `crtr node new --json` success. */
122
+ export interface SpawnResponse {
123
+ node_id: string;
124
+ name: string;
125
+ window?: string;
126
+ session?: string;
127
+ status: string;
128
+ follow_up: string;
129
+ }
130
+ /** `crtr node msg --json` success. */
131
+ export interface MessageResponse {
132
+ mode: string;
133
+ armed: boolean;
134
+ target: string;
135
+ delivered?: boolean;
136
+ revived?: boolean;
137
+ guidance: string;
138
+ }
139
+ /** `crtr node lifecycle revive --json` success. */
140
+ export interface ReviveResponse {
141
+ window: null;
142
+ session: string | null;
143
+ resumed: boolean;
144
+ ready: boolean;
145
+ }
146
+ /** `crtr node lifecycle close --json` success. */
147
+ export interface CloseResponse {
148
+ closed: boolean;
149
+ node_id: string;
150
+ count: number;
151
+ closed_ids: string[];
152
+ spared: string[];
153
+ }
154
+ /** `crtr node new` body. Always headless; `root` toggles a resident node. */
155
+ export interface SpawnRequest {
156
+ prompt: string;
157
+ kind: string;
158
+ mode?: NodeMode;
159
+ root?: boolean;
160
+ cwd?: string;
161
+ name?: string;
162
+ model?: string;
163
+ parent?: string;
164
+ }
165
+ /** `crtr node msg` body. */
166
+ export interface MessageRequest {
167
+ body: string;
168
+ tier?: string;
169
+ }
170
+ /** `crtr node lifecycle revive` body. */
171
+ export interface ReviveRequest {
172
+ fresh?: boolean;
173
+ }
174
+ /**
175
+ * The five resolution flows (design §5.2). humanloop's `InteractionKind` also
176
+ * has `review`; the deck loader normalizes it (and any unknown/absent kind)
177
+ * onto one of these five so a deck always renders through a known flow.
178
+ */
179
+ export type DeckKind = 'notify' | 'validation' | 'decision' | 'context' | 'error';
180
+ /** One option of a decision/validation interaction (humanloop InteractionOption). */
181
+ export interface DeckOption {
182
+ id: string;
183
+ label: string;
184
+ /** The option's consequence/explanation (humanloop `description`). */
185
+ description?: string;
186
+ }
187
+ /** One interaction within a deck (humanloop Interaction, normalized for the web). */
188
+ export interface DeckInteraction {
189
+ id: string;
190
+ title: string;
191
+ subtitle?: string;
192
+ /** Markdown body (bodyPath already inlined by `crtr human deck`). */
193
+ body?: string;
194
+ kind: DeckKind;
195
+ options: DeckOption[];
196
+ multiSelect: boolean;
197
+ allowFreetext: boolean;
198
+ freetextLabel?: string;
199
+ }
200
+ /**
201
+ * A pending ask, ranked in the inbox list (design §5.2). Provenance carries
202
+ * BOTH the conversation-facing summary (rendered by default, never the node
203
+ * id alone) and the raw asking node, shown alongside it.
204
+ */
205
+ export interface DeckSummary {
206
+ /** Opaque, stable inbox-entry id (base64url of the interaction dir). */
207
+ id: string;
208
+ /** The interaction job id (basename of the interaction dir). */
209
+ job_id: string;
210
+ /** The first interaction's kind — the glyph + flow for the row. */
211
+ kind: DeckKind;
212
+ title: string;
213
+ subtitle?: string;
214
+ /** ISO-8601 — when the ask started blocking (drives the wait duration + rank). */
215
+ blocked_since: string;
216
+ /** The conversation (spine root) this ask belongs to. */
217
+ conversation_id: string;
218
+ conversation_title: string;
219
+ /** The node that raised the ask. */
220
+ asking_node_id: string;
221
+ asking_node_name: string;
222
+ /** The asking node's cwd — sub-DAG scoping. */
223
+ cwd: string;
224
+ /** How many interactions the deck holds (a multi-question deck). */
225
+ interaction_count: number;
226
+ }
227
+ /** The full deck for a resolution flow (`crtr human deck <id> --json`). */
228
+ export interface DeckDetail extends DeckSummary {
229
+ interactions: DeckInteraction[];
230
+ }
231
+ /** One interaction's answer (humanloop InteractionResponse). */
232
+ export interface DeckAnswer {
233
+ id: string;
234
+ selectedOptionId?: string;
235
+ selectedOptionIds?: string[];
236
+ freetext?: string;
237
+ optionComments?: Record<string, string>;
238
+ }
239
+ /** `crtr human resolve <id> --json` body. */
240
+ export interface ResolveDeckRequest {
241
+ responses: DeckAnswer[];
242
+ }
243
+ /** `crtr human resolve <id> --json` success. */
244
+ export interface ResolveDeckResponse {
245
+ resolved: true;
246
+ job_id: string;
247
+ delivered: boolean;
248
+ }
249
+ /**
250
+ * Engine state carried in `snapshot.state` — the broker snapshot's `state`
251
+ * (mirrors `BrokerSnapshot.state`), reconstructed here in pi/primitive types so
252
+ * the SPA never imports the broker-protocol `BrokerSnapshot`. The SPA's
253
+ * streaming/idle indicator reads `isStreaming` (spec C.10).
254
+ */
255
+ export interface SessionState {
256
+ sessionId: string;
257
+ sessionFile: string | null;
258
+ model: string | null;
259
+ isStreaming: boolean;
260
+ thinkingLevel: ThinkingLevel;
261
+ steeringMode: 'all' | 'one-at-a-time';
262
+ followUpMode: 'all' | 'one-at-a-time';
263
+ sessionName: string | null;
264
+ autoCompactionEnabled: boolean;
265
+ pendingMessageCount: number;
266
+ }
267
+ /** Web role of one browser tab's session connection. */
268
+ export type WebRole = 'observer' | 'controller';
269
+ /** Upstream broker connectivity, surfaced to the tab (spec §6.2). */
270
+ export type BrokerStatus = 'connected' | 'reconnecting' | 'down' | 'revived';
271
+ /** Response payload to an extension dialog (`RpcExtensionUIResponse` minus id/type). */
272
+ export type DialogResponseValue = {
273
+ value: string;
274
+ } | {
275
+ confirmed: boolean;
276
+ } | {
277
+ cancelled: true;
278
+ };
279
+ /**
280
+ * A typed content block within a view tab. Discriminated union of three
281
+ * block kinds: inline/sourced markdown, a KPI grid, and a bar-list chart.
282
+ */
283
+ export type ViewBlock = {
284
+ kind: 'markdown';
285
+ source: {
286
+ node_id: string;
287
+ path: string;
288
+ } | {
289
+ inline: string;
290
+ };
291
+ } | {
292
+ kind: 'kpis';
293
+ items: {
294
+ label: string;
295
+ value: string;
296
+ unit?: string;
297
+ sub?: string;
298
+ }[];
299
+ } | {
300
+ kind: 'barlist';
301
+ title: string;
302
+ rows: {
303
+ label: string;
304
+ value: number;
305
+ max?: number;
306
+ note?: string;
307
+ }[];
308
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Shared wire types for the crouter web client/server split.
3
+ *
4
+ * The SPA imports the pi payload types it needs from this module — never from
5
+ * broker-protocol internals. The type-only re-exports below keep the web
6
+ * package's shared payload shapes in one place.
7
+ */
8
+ export {};
@@ -2,6 +2,7 @@ import { defineLeaf } from '../../core/command.js';
2
2
  import { CrtrError, notFound } from '../../core/errors.js';
3
3
  import { resolveMemoryDoc } from '../../core/memory-resolver.js';
4
4
  import { parseSubstrateDoc } from '../../core/substrate/index.js';
5
+ import { interpolateNodePaths } from '../../core/canvas/paths.js';
5
6
  import { readText } from '../../core/fs-utils.js';
6
7
  import { MEMORY_KINDS } from './shared.js';
7
8
  export const readLeaf = defineLeaf({
@@ -51,7 +52,13 @@ export const readLeaf = defineLeaf({
51
52
  : 'knowledge';
52
53
  // --kind asserts the resolved kind; a mismatch falls through to not-found.
53
54
  if (kindFilter === undefined || kind === kindFilter) {
54
- const content = includeFrontmatter ? readText(doc.path) : doc.body;
55
+ const raw = includeFrontmatter ? readText(doc.path) : doc.body;
56
+ // In-node reads (CRTR_NODE_ID set) get the literal `$CRTR_CONTEXT_DIR`
57
+ // token swapped for this node's real absolute path, so agents don't
58
+ // paste it into the non-expanding Write tool. A human/contributor
59
+ // reading generically (env unset) keeps the token literal.
60
+ const nodeId = process.env['CRTR_NODE_ID'];
61
+ const content = nodeId ? interpolateNodePaths(raw, nodeId) : raw;
55
62
  return {
56
63
  name: doc.name,
57
64
  kind,
@@ -25,6 +25,16 @@ export declare function viewSocketPath(nodeId: string): string;
25
25
  export declare function nodesRoot(): string;
26
26
  export declare function nodeDir(nodeId: string): string;
27
27
  export declare function contextDir(nodeId: string): string;
28
+ /** Replace the literal env-var tokens `$CRTR_CONTEXT_DIR` / `$CRTR_NODE_ID` in
29
+ * agent-facing doc text with the node's real values, so a doc surfaced INTO a
30
+ * node hands it a concrete absolute path instead of a shell-only string. The
31
+ * env vars only expand in bash; an agent that pastes `$CRTR_CONTEXT_DIR/x.md`
32
+ * into the (non-expanding) Write tool creates a literal `$CRTR_CONTEXT_DIR/`
33
+ * dir under the project repo. The resolved absolute path works in both bash and
34
+ * Write, so this substitution is strictly safer. Callers must only apply it
35
+ * when they have a concrete node to render for (never on generic, node-less
36
+ * output). Pure string work — no I/O. */
37
+ export declare function interpolateNodePaths(text: string, nodeId: string): string;
28
38
  export declare function jobDir(nodeId: string): string;
29
39
  export declare function reportsDir(nodeId: string): string;
30
40
  /** Overflow store for inbox entries whose inline body exceeds the digest's
@@ -69,6 +69,20 @@ export function nodeDir(nodeId) {
69
69
  export function contextDir(nodeId) {
70
70
  return join(nodeDir(nodeId), 'context');
71
71
  }
72
+ /** Replace the literal env-var tokens `$CRTR_CONTEXT_DIR` / `$CRTR_NODE_ID` in
73
+ * agent-facing doc text with the node's real values, so a doc surfaced INTO a
74
+ * node hands it a concrete absolute path instead of a shell-only string. The
75
+ * env vars only expand in bash; an agent that pastes `$CRTR_CONTEXT_DIR/x.md`
76
+ * into the (non-expanding) Write tool creates a literal `$CRTR_CONTEXT_DIR/`
77
+ * dir under the project repo. The resolved absolute path works in both bash and
78
+ * Write, so this substitution is strictly safer. Callers must only apply it
79
+ * when they have a concrete node to render for (never on generic, node-less
80
+ * output). Pure string work — no I/O. */
81
+ export function interpolateNodePaths(text, nodeId) {
82
+ return text
83
+ .replaceAll('$CRTR_CONTEXT_DIR', contextDir(nodeId))
84
+ .replaceAll('$CRTR_NODE_ID', nodeId);
85
+ }
72
86
  export function jobDir(nodeId) {
73
87
  return join(nodeDir(nodeId), 'job');
74
88
  }
@@ -48,6 +48,7 @@
48
48
  // (parseSubstrateDoc returns null for a non-substrate doc; per-file node-local
49
49
  // loads are wrapped), and tree construction is pure string work.
50
50
  import { relative, sep } from 'node:path';
51
+ import { interpolateNodePaths } from '../canvas/paths.js';
51
52
  import { listAllMemoryDocs, resolveMemoryDoc } from '../memory-resolver.js';
52
53
  import { subKindsAvailableTo } from '../config.js';
53
54
  import { parseFrontmatterGeneric } from '../frontmatter.js';
@@ -504,7 +505,10 @@ function buildPreferencesBlock(subject) {
504
505
  }
505
506
  export function renderPreferencesSection(nodeId) {
506
507
  const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
507
- return buildPreferencesBlock(subject).block;
508
+ // Surfaced INTO this node: swap the literal `$CRTR_CONTEXT_DIR` env-var token
509
+ // for the node's real absolute path so agents don't paste it into the
510
+ // non-expanding Write tool (see interpolateNodePaths).
511
+ return interpolateNodePaths(buildPreferencesBlock(subject).block, nodeId);
508
512
  }
509
513
  export function renderPreferencesForSubject(subject) {
510
514
  return buildPreferencesBlock(subject);
@@ -543,5 +547,6 @@ export function renderKnowledgeBlock(nodeId) {
543
547
  if (tree !== '')
544
548
  out += `\n\n${tree}`;
545
549
  out += `\n\n${KNOWLEDGE_OUTRO}`;
546
- return `<memory kind="knowledge">\n${out}\n</memory>`;
550
+ // Surfaced INTO this node: swap literal `$CRTR_CONTEXT_DIR` for the real path.
551
+ return interpolateNodePaths(`<memory kind="knowledge">\n${out}\n</memory>`, nodeId);
547
552
  }