@flowy-agent-store/protocol 0.1.0-beta.2 → 0.1.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -6,7 +6,32 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
6
6
  * The chat surface uses only opaque public conversation/message IDs. Internal
7
7
  * execution/session/step/attempt IDs never appear in this module.
8
8
  */
9
- const APP_SERVER_PROTOCOL_VERSION = "2026-08-26";
9
+ /**
10
+ * A contract **fingerprint**, not a version number: it must differ from the
11
+ * previous value on any wire change at all, additive included. The shape is
12
+ * `fp-<n>` — a plain counter, so a bump just increments it and no value is ever
13
+ * reused by accident.
14
+ *
15
+ * So it is a **label, not a version**: neither a release number nor a date.
16
+ * Until `fp-1` the values were date stamps (kept below as history), and those
17
+ * dates were **not** the day of the change: consecutive changes advanced the
18
+ * stamp a day each, so they ran ahead of the calendar. The list below is keyed
19
+ * by *value* — read it as "which wire change is this?", never as "when did this
20
+ * ship".
21
+ *
22
+ * `2026-09-16` carried `StoreItem.published_at`; `2026-09-17` added the two MCP
23
+ * declaration write methods (`config/set-mcp`, `config/set-mcp-enabled`);
24
+ * `2026-09-18` adds `config/get-mcp`, the file editor's read of the same file;
25
+ * `2026-09-19` adds the `conversation/list-changed` notification; `2026-09-20`
26
+ * adds the Skill **file tree** read face (`skill/files`, `skill/file`) so a
27
+ * Skill's companion files are readable, not just its bounded manifest summary;
28
+ * `2026-09-21` adds the connector **call proxy** (`connector/call`), so a third
29
+ * party can run an installed MCP tool while the connection and its credentials
30
+ * stay on the host. **`fp-1` changes the shape only** (date stamp → counter): a
31
+ * `2026-…` value invites being read as a release date, and no wire behaviour
32
+ * changed with the rename.
33
+ */
34
+ const APP_SERVER_PROTOCOL_VERSION = "fp-1";
10
35
  //#endregion
11
36
  //#region src/errors.ts
12
37
  var AppServerError = class extends Error {
@@ -78,12 +103,352 @@ function isRetryableError(error) {
78
103
  return (error instanceof AppServerError || error instanceof TransportError) && error.retryable;
79
104
  }
80
105
  //#endregion
106
+ //#region src/localized.ts
107
+ /** The other language, i.e. the second link of every fallback chain. */
108
+ function otherLocalizedLang(lang) {
109
+ return lang === "zh" ? "en" : "zh";
110
+ }
111
+ /**
112
+ * Map an i18n language tag (`zh-CN`, `en-US`, `en`) to a variant language.
113
+ * Anything that is not English resolves to `zh`, matching the app default.
114
+ */
115
+ function toLocalizedLang(language) {
116
+ return (language ?? "").toLowerCase().startsWith("en") ? "en" : "zh";
117
+ }
118
+ /** `{field}_{lang}` → `{field}_{other}` → `""`. */
119
+ function pickLocalized(text, lang) {
120
+ if (!text) return "";
121
+ return text[lang]?.trim() || text[otherLocalizedLang(lang)]?.trim() || "";
122
+ }
123
+ /** Resolve a list of localized values, dropping the ones that resolve empty. */
124
+ function pickLocalizedList(list, lang) {
125
+ return (list ?? []).map((item) => pickLocalized(item, lang)).filter((value) => value.length > 0);
126
+ }
127
+ /** Non-empty string variants in fallback order. */
128
+ function variantKeys(field, lang) {
129
+ return [`${field}_${lang}`, `${field}_${otherLocalizedLang(lang)}`];
130
+ }
131
+ /** `{field}_{lang}` → `{field}_{other}` as a string, when either carries text. */
132
+ function pickVariantText(variants, field, lang) {
133
+ for (const key of variantKeys(field, lang)) {
134
+ const value = variants?.[key];
135
+ if (typeof value === "string" && value.trim().length > 0) return value;
136
+ }
137
+ }
138
+ /** `{field}_{lang}` → `{field}_{other}` as a string list, when either is non-empty. */
139
+ function pickVariantList(variants, field, lang) {
140
+ for (const key of variantKeys(field, lang)) {
141
+ const value = variants?.[key];
142
+ if (Array.isArray(value)) {
143
+ const items = value.filter((item) => typeof item === "string" && item.length > 0);
144
+ if (items.length > 0) return items;
145
+ }
146
+ }
147
+ }
148
+ /**
149
+ * Market entry tags. Per D8=A `tags_{lang}` outranks `legacy_tags_{lang}` —
150
+ * the legacy field only fills in when the current one is absent, in either
151
+ * language.
152
+ */
153
+ function pickEntryTags(variants, lang) {
154
+ return pickVariantList(variants, "tags", lang) ?? pickVariantList(variants, "legacy_tags", lang);
155
+ }
156
+ /**
157
+ * A market entry text field: variant first, then the baseline field
158
+ * (`entry.description`, `entry.name`, …) as the last link of the chain.
159
+ */
160
+ function pickEntryText(variants, field, lang, baseline) {
161
+ return pickVariantText(variants, field, lang) ?? baseline ?? void 0;
162
+ }
163
+ //#endregion
164
+ //#region src/conversation-events.ts
165
+ function stringValue(value) {
166
+ return typeof value === "string" ? value : null;
167
+ }
168
+ function numberValue(value) {
169
+ return typeof value === "number" ? value : null;
170
+ }
171
+ function isRecord(value) {
172
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
173
+ }
174
+ /**
175
+ * Strict boolean reader: a wire value that is not a real boolean reads as
176
+ * `null` (unknown) rather than being coerced — "retryable" must never be
177
+ * guessed from a truthy string.
178
+ */
179
+ function booleanValue(value) {
180
+ return typeof value === "boolean" ? value : null;
181
+ }
182
+ /** Flatten a message/activity body to plain text, JSON-encoding anything else. */
183
+ function contentToText(value) {
184
+ if (typeof value === "string") return value;
185
+ if (isRecord(value) && typeof value.content === "string") return value.content;
186
+ return value == null ? "" : JSON.stringify(value);
187
+ }
188
+ /** Activity bodies may be nested one level down or JSON-encoded as a string. */
189
+ function unwrapContent(value) {
190
+ if (typeof value === "string") try {
191
+ return JSON.parse(value);
192
+ } catch {
193
+ return value;
194
+ }
195
+ if (isRecord(value)) {
196
+ const content = value.content;
197
+ if (typeof content === "string") try {
198
+ return JSON.parse(content);
199
+ } catch {
200
+ return value;
201
+ }
202
+ }
203
+ return value;
204
+ }
205
+ /** Shape a thinking body (`message.thinking`, or `message.activity` kind `thinking`). */
206
+ function thinkingData(value) {
207
+ if (isRecord(value) && typeof value.content === "string") return {
208
+ content: value.content,
209
+ subject: stringValue(value.subject),
210
+ status: stringValue(value.status),
211
+ duration: numberValue(value.duration) ?? numberValue(value.duration_ms)
212
+ };
213
+ const candidate = unwrapContent(value);
214
+ if (!isRecord(candidate)) return {
215
+ content: typeof candidate === "string" ? candidate : "",
216
+ subject: null,
217
+ status: null,
218
+ duration: null
219
+ };
220
+ return {
221
+ content: stringValue(candidate.content) ?? "",
222
+ subject: stringValue(candidate.subject),
223
+ status: stringValue(candidate.status),
224
+ duration: numberValue(candidate.duration) ?? numberValue(candidate.duration_ms)
225
+ };
226
+ }
227
+ /** Shape a tips body; `type` is accepted as an alias of `tip_type`. */
228
+ function tipsData(value) {
229
+ const readTip = (data) => {
230
+ const content = stringValue(data.content);
231
+ const tipType = stringValue(data.tip_type) ?? stringValue(data.type);
232
+ if (content === null && tipType === null) return null;
233
+ return {
234
+ content: content ?? "",
235
+ tipType: tipType ?? "info"
236
+ };
237
+ };
238
+ if (isRecord(value)) {
239
+ const direct = readTip(value);
240
+ if (direct) return direct;
241
+ }
242
+ const candidate = unwrapContent(value);
243
+ if (isRecord(candidate)) {
244
+ const parsed = readTip(candidate);
245
+ if (parsed) return parsed;
246
+ }
247
+ if (candidate != null && !isRecord(candidate)) return {
248
+ content: String(candidate),
249
+ tipType: "info"
250
+ };
251
+ return {
252
+ content: "",
253
+ tipType: "info"
254
+ };
255
+ }
256
+ /** Shape a tool-call body; `null` when the body carries nothing tool-shaped. */
257
+ function toolCallData(value) {
258
+ const candidate = unwrapContent(value);
259
+ if (!isRecord(candidate)) return null;
260
+ const name = stringValue(candidate.name) ?? stringValue(candidate.tool_name) ?? stringValue(candidate.tool);
261
+ if (!name && candidate.args === void 0 && candidate.arguments === void 0 && candidate.output === void 0 && candidate.result === void 0) return null;
262
+ return {
263
+ name,
264
+ args: candidate.args ?? candidate.arguments ?? candidate.input,
265
+ output: candidate.output ?? candidate.result,
266
+ status: stringValue(candidate.status)
267
+ };
268
+ }
269
+ /**
270
+ * Shape a plan body; `null` when there is no usable step list.
271
+ *
272
+ * `step` is accepted as an alias of `content`: the tool's own argument is named
273
+ * `step`, and the producer normalizes it to `content` for the plan row — a row
274
+ * written before that normalization (or by a producer that skipped it) would
275
+ * otherwise render as an empty plan.
276
+ */
277
+ function planData(value) {
278
+ const candidate = unwrapContent(value);
279
+ if (!isRecord(candidate) || !Array.isArray(candidate.entries)) return null;
280
+ const entries = candidate.entries.flatMap((raw) => {
281
+ if (!isRecord(raw)) return [];
282
+ const content = stringValue(raw.content) ?? stringValue(raw.step);
283
+ if (!content) return [];
284
+ return [{
285
+ content,
286
+ status: planStepStatus(raw.status)
287
+ }];
288
+ });
289
+ return entries.length > 0 ? { entries } : null;
290
+ }
291
+ /** Unknown/absent step statuses read as `pending` — never as "done". */
292
+ function planStepStatus(value) {
293
+ return value === "completed" || value === "in_progress" ? value : "pending";
294
+ }
295
+ /** Shape a `context.usage` payload; `null` when nothing was actually measured. */
296
+ function parseContextUsage(value) {
297
+ if (!isRecord(value)) return null;
298
+ const used = typeof value.used_tokens === "number" ? value.used_tokens : 0;
299
+ const window = typeof value.window_tokens === "number" ? value.window_tokens : 0;
300
+ if (used <= 0 || window <= 0) return null;
301
+ const rawPercent = typeof value.percent === "number" ? value.percent : used / window * 100;
302
+ return {
303
+ used_tokens: used,
304
+ window_tokens: window,
305
+ percent: Math.min(100, Math.max(0, rawPercent)),
306
+ updated_at: typeof value.updated_at === "number" ? value.updated_at : Date.now(),
307
+ source: typeof value.source === "string" ? value.source : "measured"
308
+ };
309
+ }
310
+ /**
311
+ * Shape a per-turn `usage` body (W9 / R14); `null` when the runtime reported
312
+ * nothing usable.
313
+ *
314
+ * A missing side is **unknown**, not zero: half a bill is not a bill, so a
315
+ * frame that only carries `input_tokens` decodes to `null` rather than to a
316
+ * total that silently drops the output side. Same rule for an all-zero report —
317
+ * "no tokens reported" must never render as "this turn was free".
318
+ */
319
+ function parseTurnUsage(value) {
320
+ if (!isRecord(value)) return null;
321
+ const input = typeof value.input_tokens === "number" ? value.input_tokens : null;
322
+ const output = typeof value.output_tokens === "number" ? value.output_tokens : null;
323
+ if (input === null || output === null) return null;
324
+ if (input <= 0 && output <= 0) return null;
325
+ return {
326
+ input_tokens: input,
327
+ output_tokens: output,
328
+ total_tokens: typeof value.total_tokens === "number" ? value.total_tokens : input + output
329
+ };
330
+ }
331
+ /**
332
+ * Decode one `conversation/event` frame into a typed body.
333
+ *
334
+ * An activity frame whose `kind` is `thinking` is normalized to
335
+ * `message.thinking` (the server has two spellings for the same thing).
336
+ */
337
+ function decodeConversationEvent(event) {
338
+ const payload = event.payload ?? {};
339
+ const messageId = () => stringValue(payload.message_id);
340
+ const createdAt = () => numberValue(payload.created_at);
341
+ if (event.event_type === "message.activity" && stringValue(payload.kind) === "thinking") return {
342
+ kind: "message.thinking",
343
+ event,
344
+ messageId: messageId(),
345
+ createdAt: createdAt(),
346
+ thinking: thinkingData(payload),
347
+ replace: payload.replace === true
348
+ };
349
+ switch (event.event_type) {
350
+ case "message.created": return {
351
+ kind: "message.created",
352
+ event,
353
+ messageId: messageId(),
354
+ createdAt: createdAt(),
355
+ content: payload.content ?? ""
356
+ };
357
+ case "message.delta": return {
358
+ kind: "message.delta",
359
+ event,
360
+ messageId: messageId(),
361
+ createdAt: createdAt(),
362
+ delta: stringValue(payload.content) ?? "",
363
+ replace: payload.replace === true
364
+ };
365
+ case "message.thinking": return {
366
+ kind: "message.thinking",
367
+ event,
368
+ messageId: messageId(),
369
+ createdAt: createdAt(),
370
+ thinking: thinkingData(payload),
371
+ replace: payload.replace === true
372
+ };
373
+ case "message.tool": return {
374
+ kind: "message.tool",
375
+ event,
376
+ messageId: messageId(),
377
+ createdAt: createdAt(),
378
+ tool: toolCallData(payload)
379
+ };
380
+ case "message.tips": return {
381
+ kind: "message.tips",
382
+ event,
383
+ messageId: messageId(),
384
+ createdAt: createdAt(),
385
+ tips: tipsData(payload)
386
+ };
387
+ case "message.error": return {
388
+ kind: "message.error",
389
+ event,
390
+ messageId: messageId(),
391
+ createdAt: createdAt(),
392
+ message: stringValue(payload.message),
393
+ code: stringValue(payload.code),
394
+ retryable: booleanValue(payload.retryable)
395
+ };
396
+ case "message.activity": {
397
+ const activityKind = stringValue(payload.kind) ?? "activity";
398
+ return {
399
+ kind: "message.activity",
400
+ event,
401
+ messageId: messageId(),
402
+ createdAt: createdAt(),
403
+ activityKind,
404
+ content: payload.content ?? "",
405
+ usage: activityKind === "turn_completed" ? parseTurnUsage(payload.usage) : null
406
+ };
407
+ }
408
+ case "turn.status": return {
409
+ kind: "turn.status",
410
+ event,
411
+ running: stringValue(payload.status) === "running"
412
+ };
413
+ case "context.usage": return {
414
+ kind: "context.usage",
415
+ event,
416
+ usage: parseContextUsage(payload.context_usage)
417
+ };
418
+ default: return {
419
+ kind: "unknown",
420
+ event,
421
+ eventType: event.event_type
422
+ };
423
+ }
424
+ }
425
+ //#endregion
81
426
  exports.APP_SERVER_PROTOCOL_VERSION = APP_SERVER_PROTOCOL_VERSION;
82
427
  exports.AppServerError = AppServerError;
83
428
  exports.ProtocolError = ProtocolError;
84
429
  exports.RequestTimeoutError = RequestTimeoutError;
85
430
  exports.TransportError = TransportError;
431
+ exports.booleanValue = booleanValue;
432
+ exports.contentToText = contentToText;
433
+ exports.decodeConversationEvent = decodeConversationEvent;
86
434
  exports.formatError = formatError;
87
435
  exports.isAppServerError = isAppServerError;
436
+ exports.isRecord = isRecord;
88
437
  exports.isRetryableError = isRetryableError;
89
438
  exports.isRetryableTransportError = isRetryableTransportError;
439
+ exports.numberValue = numberValue;
440
+ exports.otherLocalizedLang = otherLocalizedLang;
441
+ exports.parseContextUsage = parseContextUsage;
442
+ exports.parseTurnUsage = parseTurnUsage;
443
+ exports.pickEntryTags = pickEntryTags;
444
+ exports.pickEntryText = pickEntryText;
445
+ exports.pickLocalized = pickLocalized;
446
+ exports.pickLocalizedList = pickLocalizedList;
447
+ exports.pickVariantList = pickVariantList;
448
+ exports.pickVariantText = pickVariantText;
449
+ exports.planData = planData;
450
+ exports.stringValue = stringValue;
451
+ exports.thinkingData = thinkingData;
452
+ exports.tipsData = tipsData;
453
+ exports.toLocalizedLang = toLocalizedLang;
454
+ exports.toolCallData = toolCallData;