@prismshadow/penguin-server 0.2.4 → 0.2.5

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,6 @@
1
1
  import { OmniMessage, ToolCallPayload, CompactionMode } from '@prismshadow/penguin-core/omnimessage';
2
2
  import { ThinkingLevelName, ToolDefinitionConfig, MCPServerConfig } from '@prismshadow/penguin-core/interfaces';
3
+ import { VersionReport } from '@prismshadow/penguin-core';
3
4
 
4
5
  /**
5
6
  * Web API DTO contract — request/response types shared between server routes and the
@@ -69,9 +70,10 @@ interface MeResponse {
69
70
  * How THIS session was established. Distinct from desktopMode: a browser signed into a
70
71
  * desktop-mode server holds a "password" session and must still provide the old
71
72
  * password when changing it — only "desktop" sessions (opened by the shell's one-shot
72
- * token) may omit it.
73
+ * token) may omit it. "token" marks a request authenticated by the local API token's
74
+ * Bearer header (the CLI and agent-driven calls).
73
75
  */
74
- sessionVia: "password" | "desktop";
76
+ sessionVia: "password" | "desktop" | "token";
75
77
  /**
76
78
  * The upload limits currently in force, so the composer can refuse an oversize pick before
77
79
  * reading it and can name the real number in the message. They are admin-settable and ride
@@ -216,12 +218,6 @@ interface UiPrefs {
216
218
  lastProjectId?: string;
217
219
  /** Whether the "no API key configured" guide has already been shown: once ever (on first visit to the chat page). */
218
220
  credentialGuideSeen?: boolean;
219
- /**
220
- * Also list CLI-created Sessions in the sidebar (`cli=1` on the sessions list). Default
221
- * off: the list then serves web rows straight from the DB, with no Trace-directory
222
- * scanning (#139).
223
- */
224
- showCliSessions?: boolean;
225
221
  /** The initial-password notice banner (app layout) was permanently dismissed by the user. */
226
222
  initialPasswordBannerDismissed?: boolean;
227
223
  /**
@@ -565,6 +561,53 @@ interface DefaultModelUpdateRequest {
565
561
  interface DefaultModelResponse {
566
562
  defaultModel: ModelRefDto;
567
563
  }
564
+ /**
565
+ * How the authorization code travels back. `callback` sends the browser to a harness URL
566
+ * the provider redirects to; `manual` asks the provider for a one-time code the user copies
567
+ * back by hand, for the case where that redirect cannot reach the harness.
568
+ */
569
+ type ModelOAuthMode = "callback" | "manual";
570
+ /**
571
+ * POST /api/projects/:p/model-oauth/start (owner): opens a flow for one provider group.
572
+ * The group must declare an authorization flow in the built-in catalog — the endpoints it
573
+ * uses come from there, never from this request.
574
+ */
575
+ interface ModelOAuthStartRequest {
576
+ /** Provider group id (must be a catalog group that publishes a key-minting flow). */
577
+ provider: string;
578
+ /** Defaults to `callback`. */
579
+ mode?: ModelOAuthMode;
580
+ }
581
+ /** The opaque flow handle plus the page to send the user to. No secret of the flow's is included. */
582
+ interface ModelOAuthStartResponse {
583
+ flowId: string;
584
+ authorizeUrl: string;
585
+ }
586
+ /** Why a flow failed, as a code the frontend phrases; never carries a code, a verifier or a key. */
587
+ type ModelOAuthErrorCode = "invalid_request" | "code_rejected" | "upstream_failed" | "unreachable" | "apply_failed";
588
+ /**
589
+ * GET /api/projects/:p/model-oauth/:flowId (owner): where a flow stands. Unknown, expired,
590
+ * and other users' flows are all 404 alike.
591
+ */
592
+ interface ModelOAuthStatusResponse {
593
+ status: "pending" | "done" | "error";
594
+ /** The provider group the flow mints a key for. */
595
+ provider: string;
596
+ error?: ModelOAuthErrorCode;
597
+ }
598
+ /**
599
+ * POST /api/projects/:p/model-oauth/:flowId/code (owner): redeems a code the user pasted,
600
+ * the manual counterpart of the redirect callback. A flow is single-use either way.
601
+ */
602
+ interface ModelOAuthCodeRequest {
603
+ code: string;
604
+ }
605
+ /** Redemption outcome; `applied` counts the models the minted key was written to. */
606
+ interface ModelOAuthCodeResponse {
607
+ ok: boolean;
608
+ applied?: number;
609
+ error?: ModelOAuthErrorCode;
610
+ }
568
611
  /**
569
612
  * Per-Project new-chat defaults: prefill for the chat draft page. Every key is optional —
570
613
  * an absent key means "not set" (the pre-existing behavior). Serves as the GET response,
@@ -673,6 +716,28 @@ interface AgentCreateRequest {
673
716
  /** Display name; defaults to agentId. */
674
717
  name?: string;
675
718
  description?: string;
719
+ /**
720
+ * Library Skill names installed into the new Agent, seeding it at creation. Every name must
721
+ * exist in the library (404 `unknown_skill` otherwise, before anything is created); omitted or
722
+ * empty leaves the Agent with no Skills, which is what a plain Agent gets by default.
723
+ */
724
+ skills?: string[];
725
+ /**
726
+ * Skills imported from a directory on disk instead of the library. `skillsDirectory` is the
727
+ * absolute path the user picked and `directorySkills` are the names to install from it, read
728
+ * back from `.agents/skills` / `.claude/skills` at creation time. Both are required together,
729
+ * and every name must still be there (404 `unknown_skill` otherwise, before anything is
730
+ * created) — the client sends names, never Skill content.
731
+ */
732
+ skillsDirectory?: string;
733
+ directorySkills?: string[];
734
+ /**
735
+ * Base64 of an exported Agent State snapshot package (`.tar.gz`): the new Agent is
736
+ * initialized from the package instead of the default template. Mutually exclusive with
737
+ * skill seeding (`skills` / `skillsDirectory`) — the package carries its own skills.
738
+ * Explicit `name` / `description` override the package's values; absent ones keep them.
739
+ */
740
+ dataBase64?: string;
676
741
  }
677
742
  interface AgentCreateResponse {
678
743
  agent: AgentSummary;
@@ -765,13 +830,13 @@ interface AgentConfigResponse {
765
830
  }
766
831
  /**
767
832
  * POST …/config/kernel-update result: the smart merge's outcome (core's applyKernelUpdate).
768
- * Paths are dotted config leaves (`system_prompt`, `memory.prompt`, `tools.builtin.<name>`…)
769
- * in defaults-traversal order; the client maps them to display names.
833
+ * The entries are Agent settings **tabs** (`prompt`, `runtime`, `tools`, `skills`, `memory`,
834
+ * `vault`, `schedules`) in the settings page's tab order; the client maps them to tab labels.
770
835
  */
771
836
  interface AgentKernelUpdateResponse {
772
- /** Leaves advanced to the new default (previously missing, or an untouched old default). */
837
+ /** Tabs advanced to the current defaults (previously absent, or an untouched old default). */
773
838
  advanced: string[];
774
- /** Leaves kept because the stored value matches no recorded defaults generation (user customizations, kept conservatively). */
839
+ /** Tabs kept whole because they match no recorded default (customized, kept conservatively). */
775
840
  kept: string[];
776
841
  /** The kernel stamp written (the current defaults generation). */
777
842
  kernelVersion: string;
@@ -979,6 +1044,8 @@ interface SessionInfo {
979
1044
  * history itself when it needs it.
980
1045
  */
981
1046
  tracePath?: string;
1047
+ /** Present when the Session has an ENABLED messaging binding: its channel (the sidebar row's per-channel indicator). */
1048
+ messagingChannel?: MessagingChannel;
982
1049
  }
983
1050
  /**
984
1051
  * Session list category, the sidebar's four-way split applied server-side: archived wins
@@ -1014,6 +1081,17 @@ interface DirListResponse {
1014
1081
  /** Subdirectory list (sorted by name, files excluded). */
1015
1082
  entries: DirEntryInfo[];
1016
1083
  }
1084
+ /** One Skill found in a picked directory: metadata plus which of the two layouts it came from. */
1085
+ interface DirectorySkillItem extends SkillMetadataItem {
1086
+ /** `.agents/skills` or `.claude/skills` — shown so the origin of an offered Skill is visible. */
1087
+ source: string;
1088
+ }
1089
+ interface DirectorySkillsResponse {
1090
+ /** Absolute path that was scanned (realpath). */
1091
+ path: string;
1092
+ /** Installable Skills found under the directory's Skill layouts; empty when it carries none. */
1093
+ skills: DirectorySkillItem[];
1094
+ }
1017
1095
  interface SessionCreateRequest {
1018
1096
  /** Upstream id of the session's model; always sent together with provider. Omit both for the Project's default Model. */
1019
1097
  modelId?: string;
@@ -1027,6 +1105,12 @@ interface SessionCreateRequest {
1027
1105
  workspace?: string;
1028
1106
  /** Defaults to allow-all. */
1029
1107
  approvalMode?: ApprovalMode;
1108
+ /**
1109
+ * Creating-client hint stored on the Session row: "cli" when the CLI creates the
1110
+ * Session through the API; defaults to "web". Informational provenance only — lists
1111
+ * serve every row regardless of client.
1112
+ */
1113
+ client?: "web" | "cli";
1030
1114
  }
1031
1115
  interface SessionCreateResponse {
1032
1116
  session: SessionInfo;
@@ -1284,6 +1368,30 @@ interface PendingFollowUpInfo {
1284
1368
  /** Number of file attachments in the queued input. */
1285
1369
  files: number;
1286
1370
  }
1371
+ /**
1372
+ * One live subagent child of a session's runtime, carried on `task_state` events and the SSE
1373
+ * subscribe snapshot: the child Session id (the origin hop the stream already correlates by),
1374
+ * its background registry handle (null while it only lives inside a foreground collect
1375
+ * window), and whether a round is currently running. Only an ACTIVE parent runtime reports
1376
+ * children — after a server restart the in-process children are gone, and the empty list is
1377
+ * the truth.
1378
+ */
1379
+ interface SubagentRuntimeInfo {
1380
+ sessionId: string;
1381
+ subagentId: string | null;
1382
+ running: boolean;
1383
+ }
1384
+ /**
1385
+ * Response of POST /api/sessions/:sessionId/subagents/:childSessionId/message — a user input
1386
+ * on the child, whatever its state: `steered` = queued as a mid-run interjection, `started` =
1387
+ * began a follow-up run on the idle child, `resumed` = the released child session was revived
1388
+ * (resume-session semantics) and the message began its next round. The failure shapes are
1389
+ * HTTP statuses instead: 404 when the child's session record does not exist or cannot be
1390
+ * revived, 409 when the child cannot take the message right now.
1391
+ */
1392
+ interface SubagentMessageResponse {
1393
+ outcome: "steered" | "started" | "resumed";
1394
+ }
1287
1395
  /**
1288
1396
  * Response of the two recall endpoints — DELETE /api/sessions/:id/steer/:steerId and
1289
1397
  * DELETE /api/sessions/:id/follow-ups/:followUpId: the withdrawn message's original content,
@@ -1336,6 +1444,159 @@ interface SessionProcessInfo {
1336
1444
  interface SessionProcessesResponse {
1337
1445
  processes: SessionProcessInfo[];
1338
1446
  }
1447
+ /** Messaging channels a Session can bind to. */
1448
+ type MessagingChannel = "feishu" | "telegram";
1449
+ /** Event-connection runtime state of one binding (kept in memory, not persisted). */
1450
+ type MessagingRuntimeState = "disconnected" | "connecting" | "connected" | "error";
1451
+ interface MessagingRuntimeStatus {
1452
+ state: MessagingRuntimeState;
1453
+ /** Failure detail; present only in the `error` state. */
1454
+ lastError?: string;
1455
+ /** When the state last changed (ISO 8601); absent for a binding that never connected. */
1456
+ changedAt?: string;
1457
+ }
1458
+ /** The stored Feishu config, secret masked (plaintext never leaves the server). */
1459
+ interface FeishuBindingInfo {
1460
+ channel: "feishu";
1461
+ sessionId: string;
1462
+ appId: string;
1463
+ /**
1464
+ * Masked app secret (site-wide mask rule: `***`, or `first4…last4` for long values);
1465
+ * absent when no secret is stored (never entered, or cleared) — the binding cannot be
1466
+ * enabled until one is saved.
1467
+ */
1468
+ appSecretMasked?: string;
1469
+ baseDomain: string;
1470
+ /** Connection INTENT (the state toggle's value); new bindings start disabled, and at most one of a Session's channels is enabled. */
1471
+ enabled: boolean;
1472
+ /**
1473
+ * Whether an inbound Feishu chat is known (the bot has been messaged at least once).
1474
+ * Replies and test messages target that chat; until it exists nothing can be sent.
1475
+ */
1476
+ lastChatKnown: boolean;
1477
+ createdAt: string;
1478
+ updatedAt: string;
1479
+ }
1480
+ /** The stored Telegram config, token masked (plaintext never leaves the server). */
1481
+ interface TelegramBindingInfo {
1482
+ channel: "telegram";
1483
+ sessionId: string;
1484
+ /** The numeric bot id (the token's half before the colon) — the channel-scoped account identity, never secret. */
1485
+ botId: string;
1486
+ /**
1487
+ * Masked bot token (site-wide mask rule: `***`, or `first4…last4` for long values);
1488
+ * absent when no token is stored (cleared) — the binding cannot be enabled until one
1489
+ * is saved.
1490
+ */
1491
+ botTokenMasked?: string;
1492
+ /** Connection INTENT (the state toggle's value); new bindings start disabled, and at most one of a Session's channels is enabled. */
1493
+ enabled: boolean;
1494
+ /**
1495
+ * Whether an inbound Telegram chat is known (the bot has been messaged at least once).
1496
+ * Replies and test messages target that chat; until it exists nothing can be sent.
1497
+ */
1498
+ lastChatKnown: boolean;
1499
+ createdAt: string;
1500
+ updatedAt: string;
1501
+ }
1502
+ /** A Session's saved config for one messaging channel (`channel` is the discriminant). */
1503
+ type MessagingBindingInfo = FeishuBindingInfo | TelegramBindingInfo;
1504
+ /** One saved channel config with its event-connection runtime status. */
1505
+ interface MessagingChannelState {
1506
+ binding: MessagingBindingInfo;
1507
+ /** Only the enabled channel's connection is ever anything but disconnected. */
1508
+ status: MessagingRuntimeStatus;
1509
+ }
1510
+ /**
1511
+ * GET …/messaging response — the channel-agnostic read the channel-aware binding editor
1512
+ * loads: EVERY saved channel config (masked) with its runtime status. A Session may keep
1513
+ * both channels saved; at most one of them is enabled.
1514
+ */
1515
+ interface MessagingBindingsResponse {
1516
+ bindings: MessagingChannelState[];
1517
+ }
1518
+ /** GET / PUT …/messaging/feishu response: the Feishu config (null = not saved) plus its runtime status. */
1519
+ interface FeishuBindingResponse {
1520
+ binding: FeishuBindingInfo | null;
1521
+ status: MessagingRuntimeStatus;
1522
+ }
1523
+ /** GET / PUT …/messaging/telegram response (the Telegram narrowing of the same envelope). */
1524
+ interface TelegramBindingResponse {
1525
+ binding: TelegramBindingInfo | null;
1526
+ status: MessagingRuntimeStatus;
1527
+ }
1528
+ /**
1529
+ * PUT …/messaging/feishu — saves credentials/config ONLY, never flipping the connection
1530
+ * (exception: an enabled binding's connector restarts with the new credentials so stored
1531
+ * config and live connection never diverge). The connection toggle is POST …/state.
1532
+ */
1533
+ interface FeishuBindingPutRequest {
1534
+ appId: string;
1535
+ /** Omitted or blank keeps the stored secret (the masked value never round-trips). */
1536
+ appSecret?: string;
1537
+ /** Defaults to https://open.feishu.cn when omitted or blank. */
1538
+ baseDomain?: string;
1539
+ /**
1540
+ * Drops the STORED secret (the models-page clear idiom; a typed `appSecret` wins over
1541
+ * it). Refused with 409 `messaging_disable_before_clear` while the binding is enabled.
1542
+ */
1543
+ clearAppSecret?: boolean;
1544
+ }
1545
+ /**
1546
+ * PUT …/messaging/telegram — saves the credential ONLY, same contract as the Feishu PUT
1547
+ * (an enabled binding's connector restarts with the new token; the connection toggle is
1548
+ * POST …/state). 409 `telegram_bot_in_use` when the token's bot id is bound elsewhere.
1549
+ */
1550
+ interface TelegramBindingPutRequest {
1551
+ /** Omitted or blank keeps the stored token (the masked value never round-trips). */
1552
+ botToken?: string;
1553
+ /**
1554
+ * Drops the STORED token (the models-page clear idiom; a typed `botToken` wins over
1555
+ * it — and the row keeps its bot identity). Refused with 409
1556
+ * `messaging_disable_before_clear` while the binding is enabled.
1557
+ */
1558
+ clearBotToken?: boolean;
1559
+ }
1560
+ /**
1561
+ * POST …/messaging/<channel>/state — enable connects with the STORED credentials,
1562
+ * disable terminates. Enabling is mutually exclusive per Session: while another channel
1563
+ * is enabled it answers 409 `another_channel_enabled` (turn that one off first), and a
1564
+ * config whose secret is missing answers its channel's 400 `*_required`.
1565
+ */
1566
+ interface MessagingBindingStateRequest {
1567
+ enabled: boolean;
1568
+ }
1569
+ /** POST …/messaging/feishu/test — draft values; each omitted field falls back to the stored binding. */
1570
+ interface FeishuTestRequest {
1571
+ appId?: string;
1572
+ appSecret?: string;
1573
+ baseDomain?: string;
1574
+ }
1575
+ /** Credential-test outcome (an unreachable/rejected credential is `ok:false`, not an HTTP error). */
1576
+ interface FeishuTestResponse {
1577
+ ok: boolean;
1578
+ latencyMs?: number;
1579
+ error?: string;
1580
+ }
1581
+ /** POST …/messaging/telegram/test — a draft token, falling back to the stored binding when omitted. */
1582
+ interface TelegramTestRequest {
1583
+ botToken?: string;
1584
+ }
1585
+ /** Telegram credential-test outcome: success additionally names the bot the token signs in as. */
1586
+ interface TelegramTestResponse {
1587
+ ok: boolean;
1588
+ latencyMs?: number;
1589
+ /** The bot's `@username`, when the API reports one. */
1590
+ botUsername?: string;
1591
+ error?: string;
1592
+ }
1593
+ /**
1594
+ * POST …/messaging/<channel>/test-message — sent to the last known chat (409
1595
+ * `feishu_no_chat` / `telegram_no_chat` before one exists).
1596
+ */
1597
+ interface MessagingTestMessageResponse {
1598
+ ok: true;
1599
+ }
1339
1600
  /** Docs: /docs/server-api § "Streaming (SSE)". */
1340
1601
  type ServerEvent =
1341
1602
  /**
@@ -1356,6 +1617,12 @@ type ServerEvent =
1356
1617
  pendingSteering?: PendingSteeringInfo[];
1357
1618
  /** Queued follow-up tasks awaiting auto-start (absent = none): per-entry content + recall handle, alongside the `queued` count. */
1358
1619
  pendingFollowUps?: PendingFollowUpInfo[];
1620
+ /**
1621
+ * Live subagent children of this session's runtime (absent = none): the panel renders
1622
+ * child running marks from this structural liveness instead of parsing tool-output
1623
+ * text. Refreshed on every child run start/settle.
1624
+ */
1625
+ subagents?: SubagentRuntimeInfo[];
1359
1626
  }
1360
1627
  /** The model-generated title after the first turn has been persisted (for in-place list updates). */
1361
1628
  | {
@@ -1474,6 +1741,51 @@ interface TraceFileInfo {
1474
1741
  interface SessionTracesResponse {
1475
1742
  files: TraceFileInfo[];
1476
1743
  }
1744
+ /** One tool's share of the context: its calls plus their results. Its *definition* is counted in `toolDefs`, not here. */
1745
+ interface ContextToolShare {
1746
+ name: string;
1747
+ tokens: number;
1748
+ }
1749
+ /**
1750
+ * What the Session's current model context is made of — the part derived from its messages.
1751
+ *
1752
+ * Every token figure is an **estimate** from a character heuristic, not a tokenizer: the
1753
+ * authoritative occupancy is the last `token_usage`'s `request.total`, which says how large the
1754
+ * context is but not what fills it. Consumers should present these as shares of that measured
1755
+ * occupancy rather than as counts of their own.
1756
+ *
1757
+ * The six parts partition the context and sum to `total`; `topTools` is a ranking inside
1758
+ * `toolRequests + toolResults` and can sum to less than those two (a result whose call was not
1759
+ * recorded in the same Trace shard has no tool to be attributed to).
1760
+ */
1761
+ interface SessionContextParts {
1762
+ systemPrompt: number;
1763
+ toolDefs: number;
1764
+ userMessages: number;
1765
+ assistantMessages: number;
1766
+ toolRequests: number;
1767
+ toolResults: number;
1768
+ /** Sum of the six parts. */
1769
+ total: number;
1770
+ /** Tools ranked by the context their traffic occupies, descending; at most five. */
1771
+ topTools: ContextToolShare[];
1772
+ /**
1773
+ * A completed compaction closed the context these figures describe, and the next one has not
1774
+ * been written yet: the composition is of what was compacted away, not of what the model now
1775
+ * carries. The same state in which the chat page's context ring shows `—`.
1776
+ */
1777
+ contextClosed: boolean;
1778
+ }
1779
+ /** `GET /api/sessions/:id/context`: the message-derived composition plus where compaction will fire. */
1780
+ interface SessionContextResponse extends SessionContextParts {
1781
+ /**
1782
+ * Occupancy (tokens) at which this Session's next Request triggers context compaction: the
1783
+ * Agent's configured `compaction.max_context_length`, capped by what the model's context window
1784
+ * leaves room for. Null when compaction is disabled, when the Agent's config could not be read,
1785
+ * or when the derived threshold is not below the window — nothing to mark inside the gauge.
1786
+ */
1787
+ compactionThreshold: number | null;
1788
+ }
1477
1789
  interface TraceEventsResponse {
1478
1790
  events: OmniMessage[];
1479
1791
  offset: number;
@@ -2053,7 +2365,7 @@ interface SkillMetadataItem {
2053
2365
  icon?: string;
2054
2366
  /** Version number (natural number, frontmatter version; falls back to 1 if invalid). */
2055
2367
  version: number;
2056
- /** Update date (YYYY-MM-DD, frontmatter updated; defaults to an empty string). */
2368
+ /** Update timestamp (frontmatter updated, ISO 8601 UTC by convention; defaults to an empty string). */
2057
2369
  updated: string;
2058
2370
  }
2059
2371
  interface SkillGroupItem {
@@ -2087,17 +2399,15 @@ interface SkillArchiveInstallRequest {
2087
2399
  /** Replace an already-installed Skill of the same name (deletes its directory first). */
2088
2400
  overwrite?: boolean;
2089
2401
  }
2090
- /** GET /api/version: the running server's release identity (from core's VERSION / BUILD_DATE). */
2091
- interface VersionResponse {
2092
- version: string;
2093
- /**
2094
- * The **running** version's release date (UTC yyyy-mm-dd), stamped into core's
2095
- * BUILD_DATE at build time by the release workflow — the web's "last updated" date
2096
- * needs no network. Null for a dev/source build and for releases that predate the
2097
- * stamping (v0.1.2 and earlier): the UI then shows the version alone.
2098
- */
2099
- buildDate: string | null;
2100
- }
2402
+ /**
2403
+ * GET /api/version: the running build's identity plus this root's pushed harness, verbatim
2404
+ * from `versionReport()` — the same record `penguin version --json` prints, so the two
2405
+ * cannot drift apart. Field meanings live on {@link VersionReport} and {@link HarnessInfo};
2406
+ * the ones the web reads are `version` and `buildDate` (the stamped release date behind the
2407
+ * sidebar's "last updated", needing no network, and null for a source build or a release
2408
+ * predating the stamping — v0.1.2 and earlier — where the UI shows the version alone).
2409
+ */
2410
+ type VersionResponse = VersionReport;
2101
2411
  /**
2102
2412
  * GET /api/version/update-check: newest published release vs the running version.
2103
2413
  * Always HTTP 200 (fail-soft): a lookup failure sets `error` and leaves `latestVersion`
@@ -2181,4 +2491,4 @@ interface UpdateRunResponse {
2181
2491
  needsRestart: boolean;
2182
2492
  }
2183
2493
 
2184
- export type { AdminPasswordResetRequest, AdminUserCreateRequest, AdminUserCreateResponse, AdminUsersResponse, AgentCompactionConfigDto, AgentConfigDto, AgentConfigResponse, AgentConfigUpdateRequest, AgentCreateRequest, AgentCreateResponse, AgentImportRequest, AgentImportResponse, AgentKernelUpdateResponse, AgentMemoryConfigDto, AgentModelConfigDto, AgentSchedulesConfigDto, AgentSkillsConfigDto, AgentSkillsResponse, AgentSummary, AgentTraceDateGroup, AgentTraceFileRef, AgentTraceSessionEntry, AgentTraceSessionFile, AgentTraceSessionGroup, AgentTracesResponse, AgentVaultConfigDto, AgentsResponse, ApprovalDecisionRequest, ApprovalMode, AuthLoginRequest, AuthResponse, BenchmarkCaseScore, BenchmarkCaseSummary, BenchmarkCasesResponse, BenchmarkEvaluation, BenchmarkRunScore, BenchmarkSummary, BenchmarksResponse, CaseMaterial, ChatDefaultsDto, CommandPolicyDto, CommandPolicyRuleDto, CredentialInfo, DefaultModelResponse, DefaultModelUpdateRequest, DesktopUpdateStatus, DesktopUpdateStatusResponse, DesktopUpdaterCommandMessage, DesktopUpdaterStatusMessage, DirEntryInfo, DirListResponse, DraftShortcut, EndpointModelListRequest, EndpointModelListResponse, ErrorBody, FilesStatRequest, FilesStatResponse, GoalResponse, GoalServerEvent, GoalStateView, HistoryMessage, McpServerTestResponse, MeResponse, MemberAddRequest, MemberAddResponse, MemberInfo, MembersResponse, MemoryFileInfo, MemoryFileResponse, MemoryFilesResponse, MemoryImportMode, MemoryImportRequest, MemoryImportResponse, MemoryOverviewResponse, MemoryScopeExport, MemoryScopeInfo, MemoryTransferFile, MessagesLiveTail, MessagesPageInfo, MessagesResponse, ModelInfo, ModelPricingDto, ModelProtocolDetectRequest, ModelProtocolDetectResponse, ModelProtocolProbeDto, ModelRefDto, ModelTestRequest, ModelTestResponse, ModelUpdateEntry, ModelVisionDetectRequest, ModelVisionDetectResponse, ModelVisionOutcome, ModelsResponse, ModelsUpdateRequest, PasswordChangeRequest, PendingFollowUpInfo, PendingSteeringInfo, PrefsResponse, ProjectCreateRequest, ProjectCreateResponse, ProjectRole, ProjectSummary, ProjectUpdateRequest, ProjectUpdateResponse, ProjectsResponse, ProtocolProbeOutcome, RecalledMessageResponse, RequestSpan, RetryNowResponse, ScheduleItem, ScheduleServerEvent, ScheduleStatus, ScheduleUpsertRequest, SchedulesResponse, ServerEvent, ServerSettings, ServerSettingsResponse, ServerSettingsUpdateRequest, SessionCategory, SessionCategoryCounts, SessionCreateRequest, SessionCreateResponse, SessionForkRequest, SessionForkResponse, SessionInfo, SessionPatchRequest, SessionProcessInfo, SessionProcessesResponse, SessionResponse, SessionSource, SessionStatus, SessionTracesResponse, SessionsResponse, SkillArchiveInstallRequest, SkillGroupItem, SkillInstallRequest, SkillLibraryResponse, SkillMetadataItem, SteerRequest, TaskCreateRequest, TaskCreateResponse, TaskInputPart, ToolCallSpan, TraceAnalysisResponse, TraceEventsResponse, TraceFileInfo, TraceImportRequest, TraceImportResponse, TraceModelSegment, TraceOtherSpan, TracePosition, TraceTaskStats, TraceToolSpan, UiPrefs, UpdateCheckResponse, UpdateRunResponse, UploadLimits, UsageAgentSeries, UsageBucket, UsageEntitySeriesCounts, UsageErrorCount, UsageErrorItem, UsageErrors, UsageErrorsPage, UsageGranularity, UsageGroupBy, UsageGroupRow, UsageModelSeries, UsageResponse, UsageSeriesPoint, UsageTrendPointInTrace, UserInfo, VaultEntryInfo, VaultEntryUpdate, VaultResponse, VaultUpdateRequest, VersionResponse, WorkspaceFileEntry, WorkspaceFilesResponse };
2494
+ export type { AdminPasswordResetRequest, AdminUserCreateRequest, AdminUserCreateResponse, AdminUsersResponse, AgentCompactionConfigDto, AgentConfigDto, AgentConfigResponse, AgentConfigUpdateRequest, AgentCreateRequest, AgentCreateResponse, AgentImportRequest, AgentImportResponse, AgentKernelUpdateResponse, AgentMemoryConfigDto, AgentModelConfigDto, AgentSchedulesConfigDto, AgentSkillsConfigDto, AgentSkillsResponse, AgentSummary, AgentTraceDateGroup, AgentTraceFileRef, AgentTraceSessionEntry, AgentTraceSessionFile, AgentTraceSessionGroup, AgentTracesResponse, AgentVaultConfigDto, AgentsResponse, ApprovalDecisionRequest, ApprovalMode, AuthLoginRequest, AuthResponse, BenchmarkCaseScore, BenchmarkCaseSummary, BenchmarkCasesResponse, BenchmarkEvaluation, BenchmarkRunScore, BenchmarkSummary, BenchmarksResponse, CaseMaterial, ChatDefaultsDto, CommandPolicyDto, CommandPolicyRuleDto, ContextToolShare, CredentialInfo, DefaultModelResponse, DefaultModelUpdateRequest, DesktopUpdateStatus, DesktopUpdateStatusResponse, DesktopUpdaterCommandMessage, DesktopUpdaterStatusMessage, DirEntryInfo, DirListResponse, DirectorySkillItem, DirectorySkillsResponse, DraftShortcut, EndpointModelListRequest, EndpointModelListResponse, ErrorBody, FeishuBindingInfo, FeishuBindingPutRequest, FeishuBindingResponse, FeishuTestRequest, FeishuTestResponse, FilesStatRequest, FilesStatResponse, GoalResponse, GoalServerEvent, GoalStateView, HistoryMessage, McpServerTestResponse, MeResponse, MemberAddRequest, MemberAddResponse, MemberInfo, MembersResponse, MemoryFileInfo, MemoryFileResponse, MemoryFilesResponse, MemoryImportMode, MemoryImportRequest, MemoryImportResponse, MemoryOverviewResponse, MemoryScopeExport, MemoryScopeInfo, MemoryTransferFile, MessagesLiveTail, MessagesPageInfo, MessagesResponse, MessagingBindingInfo, MessagingBindingStateRequest, MessagingBindingsResponse, MessagingChannel, MessagingChannelState, MessagingRuntimeState, MessagingRuntimeStatus, MessagingTestMessageResponse, ModelInfo, ModelOAuthCodeRequest, ModelOAuthCodeResponse, ModelOAuthErrorCode, ModelOAuthMode, ModelOAuthStartRequest, ModelOAuthStartResponse, ModelOAuthStatusResponse, ModelPricingDto, ModelProtocolDetectRequest, ModelProtocolDetectResponse, ModelProtocolProbeDto, ModelRefDto, ModelTestRequest, ModelTestResponse, ModelUpdateEntry, ModelVisionDetectRequest, ModelVisionDetectResponse, ModelVisionOutcome, ModelsResponse, ModelsUpdateRequest, PasswordChangeRequest, PendingFollowUpInfo, PendingSteeringInfo, PrefsResponse, ProjectCreateRequest, ProjectCreateResponse, ProjectRole, ProjectSummary, ProjectUpdateRequest, ProjectUpdateResponse, ProjectsResponse, ProtocolProbeOutcome, RecalledMessageResponse, RequestSpan, RetryNowResponse, ScheduleItem, ScheduleServerEvent, ScheduleStatus, ScheduleUpsertRequest, SchedulesResponse, ServerEvent, ServerSettings, ServerSettingsResponse, ServerSettingsUpdateRequest, SessionCategory, SessionCategoryCounts, SessionContextParts, SessionContextResponse, SessionCreateRequest, SessionCreateResponse, SessionForkRequest, SessionForkResponse, SessionInfo, SessionPatchRequest, SessionProcessInfo, SessionProcessesResponse, SessionResponse, SessionSource, SessionStatus, SessionTracesResponse, SessionsResponse, SkillArchiveInstallRequest, SkillGroupItem, SkillInstallRequest, SkillLibraryResponse, SkillMetadataItem, SteerRequest, SubagentMessageResponse, SubagentRuntimeInfo, TaskCreateRequest, TaskCreateResponse, TaskInputPart, TelegramBindingInfo, TelegramBindingPutRequest, TelegramBindingResponse, TelegramTestRequest, TelegramTestResponse, ToolCallSpan, TraceAnalysisResponse, TraceEventsResponse, TraceFileInfo, TraceImportRequest, TraceImportResponse, TraceModelSegment, TraceOtherSpan, TracePosition, TraceTaskStats, TraceToolSpan, UiPrefs, UpdateCheckResponse, UpdateRunResponse, UploadLimits, UsageAgentSeries, UsageBucket, UsageEntitySeriesCounts, UsageErrorCount, UsageErrorItem, UsageErrors, UsageErrorsPage, UsageGranularity, UsageGroupBy, UsageGroupRow, UsageModelSeries, UsageResponse, UsageSeriesPoint, UsageTrendPointInTrace, UserInfo, VaultEntryInfo, VaultEntryUpdate, VaultResponse, VaultUpdateRequest, VersionResponse, WorkspaceFileEntry, WorkspaceFilesResponse };
@@ -0,0 +1,50 @@
1
+ // src/hmr/manifest.ts
2
+ import fs from "fs";
3
+ import fsp from "fs/promises";
4
+ import path from "path";
5
+ function str(value) {
6
+ return typeof value === "string" && value.length > 0 ? value : null;
7
+ }
8
+ async function readHarnessInfo(root) {
9
+ const manifest = await readManifest(root);
10
+ if (manifest === null) return null;
11
+ const bundles = {
12
+ platform: str(manifest.platform?.bundle),
13
+ cli: str(manifest.cli?.bundle),
14
+ web: str(manifest.web?.manifest)
15
+ };
16
+ if (bundles.platform === null && bundles.cli === null && bundles.web === null) return null;
17
+ const repo = str(manifest.source?.repo);
18
+ const revision = str(manifest.source?.revision);
19
+ return {
20
+ // Both halves or neither: half a provenance names nothing.
21
+ source: repo !== null && revision !== null ? { repo, revision } : null,
22
+ pushedAt: str(manifest.pushedAt),
23
+ bundles
24
+ };
25
+ }
26
+ async function readManifest(root) {
27
+ try {
28
+ const raw = await fsp.readFile(path.join(root, "hmr", "harness.json"), "utf8");
29
+ return JSON.parse(raw);
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+ async function resolveCliBundlePath(root) {
35
+ const manifest = await readManifest(root);
36
+ const bundle = manifest?.cli?.bundle;
37
+ if (typeof bundle !== "string" || bundle.length === 0) return null;
38
+ const hmrDir = path.join(root, "hmr");
39
+ const abs = path.resolve(hmrDir, bundle);
40
+ const withinHmrDir = abs === hmrDir || abs.startsWith(hmrDir + path.sep);
41
+ if (!withinHmrDir) return null;
42
+ return fs.existsSync(abs) ? abs : null;
43
+ }
44
+
45
+ export {
46
+ readHarnessInfo,
47
+ readManifest,
48
+ resolveCliBundlePath
49
+ };
50
+ //# sourceMappingURL=chunk-4SXGU7RO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hmr/manifest.ts"],"sourcesContent":["/**\n * The harness.json shape (`<root>/hmr/harness.json`) and pure readers over it.\n *\n * Split out of host.ts so a reader never needs a running HmrHost: `penguin` is a\n * separate, short-lived process per invocation (see packages/cli/src/index.ts) — it\n * has no host instance to ask, only the data root's own harness.json on disk.\n * `resolveCliBundlePath` is that reader: a plain disk read, no HTTP, no boot, no\n * in-memory state.\n *\n * ONE atomic version, THREE independent artifacts: `platform`, `cli`, and `web` are\n * always written together by HmrHost's single merged upgrade path (see host.ts's\n * persistVersion), but each is content-addressed and stored on its own — a push to\n * POST /api/hmr/upgrade carries `platform` and `cli` as two separate single-file ESM\n * sources, so `cli.bundle` points at its OWN file, a different sha from\n * `platform.bundle`. They are read back together too (see host.ts's restore): a\n * partial record (e.g. `platform` present but `web` or `cli` missing) is never\n * trusted — the whole version is treated as absent instead of partially applied.\n */\nimport fs from \"node:fs\";\nimport fsp from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { HarnessInfo } from \"@prismshadow/penguin-core\";\n\n/**\n * The committed on-disk record: a runtime restart resumes exactly this CODE (platform,\n * cli, web). It carries no STATE — a restart always boots the resumed platform bundle\n * against its own fresh initial context (bundle.context, see host.ts's PlatformBundle),\n * never a document parked from a previous run (see host.ts's module doc for why: a\n * parked doc's live-resource handles die with the process anyway, so resuming one could\n * only ever produce handles that fail to reclaim). Paths are relative to hmrDir\n * (`<root>/hmr`).\n */\nexport interface Manifest {\n platform?: { bundle: string };\n /**\n * The CLI's own bundle pointer — its own independent file (a different sha from\n * `platform.bundle`; the two are separately compiled artifacts), kept as its own\n * field so a reader that only wants \"which bundle does the CLI load right now\"\n * never has to reach into `platform` at all.\n */\n cli?: { bundle: string };\n /** One gzip(JSON.stringify({ files })) artifact, restored straight into memory. */\n web?: { manifest: string };\n /**\n * Files the pushed platform needs as REAL files on disk, unpacked under this directory\n * (relative to hmrDir). The web artifact stays in memory because it is only ever served\n * as bytes; an asset is something the platform hands to the OS — a native `.node` the\n * loader must resolve by path, a helper binary it execs — so it has to exist on the\n * filesystem, with its exec bit intact.\n */\n assets?: { dir: string };\n /**\n * Provenance of the push that produced this version, exactly as the pushing client sent\n * it (scripts/deploy.mjs fills it from its own checkout). Recorded, never executed or\n * resolved. Absent for a version pushed without it, and for every version committed\n * before the field existed — which is why every reader treats it as optional.\n */\n source?: { repo: string; revision: string };\n /** When this version was committed to the store (ISO 8601). Absent on older records. */\n pushedAt?: string;\n}\n\n/** A string field of an untrusted manifest, or null unless it is a non-empty string. */\nfunction str(value: unknown): string | null {\n return typeof value === \"string\" && value.length > 0 ? value : null;\n}\n\n/**\n * What the store has committed, for version reporting: provenance, commit time, and the\n * three artifact pointers. Null when nothing has ever been pushed to this root.\n *\n * Read defensively rather than cast: harness.json is written only by persistVersion, but a\n * truncated or hand-edited file must degrade a `penguin version` to missing fields, never\n * crash it. A record missing every artifact pointer counts as nothing pushed.\n */\nexport async function readHarnessInfo(root: string): Promise<HarnessInfo | null> {\n const manifest = await readManifest(root);\n if (manifest === null) return null;\n\n const bundles = {\n platform: str(manifest.platform?.bundle),\n cli: str(manifest.cli?.bundle),\n web: str(manifest.web?.manifest),\n };\n if (bundles.platform === null && bundles.cli === null && bundles.web === null) return null;\n\n const repo = str(manifest.source?.repo);\n const revision = str(manifest.source?.revision);\n return {\n // Both halves or neither: half a provenance names nothing.\n source: repo !== null && revision !== null ? { repo, revision } : null,\n pushedAt: str(manifest.pushedAt),\n bundles,\n };\n}\n\n/** Reads and parses `<root>/hmr/harness.json`; null when missing or corrupt (nothing committed yet). */\nexport async function readManifest(root: string): Promise<Manifest | null> {\n try {\n const raw = await fsp.readFile(path.join(root, \"hmr\", \"harness.json\"), \"utf8\");\n return JSON.parse(raw) as Manifest;\n } catch {\n return null;\n }\n}\n\n/**\n * Resolves the current CLI bundle's absolute path from a data root, or null when\n * nothing has been pushed yet: a fresh root, a missing/corrupt manifest, no `cli`\n * entry, a `cli.bundle` that isn't a non-empty string (e.g. `{\"cli\":{}}` — a\n * hand-edited or truncated harness.json), a path that resolves outside `<root>/hmr/`\n * (defense in depth: `manifest.cli.bundle` is trusted content today, written only by\n * this same process's persistVersion, but a reader must never let a malformed or\n * malicious manifest value walk it out of the store directory via `..` segments), or\n * a referenced file that no longer exists (e.g. pruned from the store — see host.ts's\n * pruneStore). A null return means \"use the packaged default\" to every caller;\n * nothing here ever throws for an ordinary unconfigured or malformed root.\n */\nexport async function resolveCliBundlePath(root: string): Promise<string | null> {\n const manifest = await readManifest(root);\n const bundle = manifest?.cli?.bundle;\n if (typeof bundle !== \"string\" || bundle.length === 0) return null;\n const hmrDir = path.join(root, \"hmr\");\n const abs = path.resolve(hmrDir, bundle);\n const withinHmrDir = abs === hmrDir || abs.startsWith(hmrDir + path.sep);\n if (!withinHmrDir) return null;\n return fs.existsSync(abs) ? abs : null;\n}\n"],"mappings":";AAkBA,OAAO,QAAQ;AACf,OAAO,SAAS;AAChB,OAAO,UAAU;AA2CjB,SAAS,IAAI,OAA+B;AAC1C,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS,IAAI,QAAQ;AACjE;AAUA,eAAsB,gBAAgB,MAA2C;AAC/E,QAAM,WAAW,MAAM,aAAa,IAAI;AACxC,MAAI,aAAa,KAAM,QAAO;AAE9B,QAAM,UAAU;AAAA,IACd,UAAU,IAAI,SAAS,UAAU,MAAM;AAAA,IACvC,KAAK,IAAI,SAAS,KAAK,MAAM;AAAA,IAC7B,KAAK,IAAI,SAAS,KAAK,QAAQ;AAAA,EACjC;AACA,MAAI,QAAQ,aAAa,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,QAAQ,KAAM,QAAO;AAEtF,QAAM,OAAO,IAAI,SAAS,QAAQ,IAAI;AACtC,QAAM,WAAW,IAAI,SAAS,QAAQ,QAAQ;AAC9C,SAAO;AAAA;AAAA,IAEL,QAAQ,SAAS,QAAQ,aAAa,OAAO,EAAE,MAAM,SAAS,IAAI;AAAA,IAClE,UAAU,IAAI,SAAS,QAAQ;AAAA,IAC/B;AAAA,EACF;AACF;AAGA,eAAsB,aAAa,MAAwC;AACzE,MAAI;AACF,UAAM,MAAM,MAAM,IAAI,SAAS,KAAK,KAAK,MAAM,OAAO,cAAc,GAAG,MAAM;AAC7E,WAAO,KAAK,MAAM,GAAG;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAcA,eAAsB,qBAAqB,MAAsC;AAC/E,QAAM,WAAW,MAAM,aAAa,IAAI;AACxC,QAAM,SAAS,UAAU,KAAK;AAC9B,MAAI,OAAO,WAAW,YAAY,OAAO,WAAW,EAAG,QAAO;AAC9D,QAAM,SAAS,KAAK,KAAK,MAAM,KAAK;AACpC,QAAM,MAAM,KAAK,QAAQ,QAAQ,MAAM;AACvC,QAAM,eAAe,QAAQ,UAAU,IAAI,WAAW,SAAS,KAAK,GAAG;AACvE,MAAI,CAAC,aAAc,QAAO;AAC1B,SAAO,GAAG,WAAW,GAAG,IAAI,MAAM;AACpC;","names":[]}
@@ -0,0 +1,14 @@
1
+ import {
2
+ readHarnessInfo
3
+ } from "./chunk-4SXGU7RO.js";
4
+
5
+ // src/version-report.ts
6
+ import { buildInfo } from "@prismshadow/penguin-core";
7
+ async function versionReport(root) {
8
+ return { ...buildInfo(), harness: await readHarnessInfo(root) };
9
+ }
10
+
11
+ export {
12
+ versionReport
13
+ };
14
+ //# sourceMappingURL=chunk-TTGGJDNA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/version-report.ts"],"sourcesContent":["/**\n * The one assembler behind `penguin version --json` and GET /api/version.\n *\n * The report has two halves that no single layer can produce. Core knows the artifact this\n * process is executing and nothing about data roots; the HMR store knows what harness was\n * pushed to a root and nothing about the process reading it. Joining them here — rather\n * than in each caller — is what makes \"the CLI and the API report the same thing\" a\n * structural fact instead of two spreads that have to be kept in step.\n *\n * It lives in the server package because that is where the harness.json format is owned\n * (see ./hmr/manifest.ts), and is exported as its own entry point so the CLI can reach it\n * without pulling in a server: the module graph here is core plus a `node:fs` read.\n */\nimport { buildInfo } from \"@prismshadow/penguin-core\";\nimport type { VersionReport } from \"@prismshadow/penguin-core\";\nimport { readHarnessInfo } from \"./hmr/manifest.js\";\n\n/**\n * The running build's identity plus the harness committed to `root`'s HMR store.\n *\n * Never throws: an unreadable or malformed store reports `harness: null`, which is also\n * what a root with nothing ever pushed to it reports. Version reporting degrades to fewer\n * facts, never to a failure.\n */\nexport async function versionReport(root: string): Promise<VersionReport> {\n return { ...buildInfo(), harness: await readHarnessInfo(root) };\n}\n"],"mappings":";;;;;AAaA,SAAS,iBAAiB;AAW1B,eAAsB,cAAc,MAAsC;AACxE,SAAO,EAAE,GAAG,UAAU,GAAG,SAAS,MAAM,gBAAgB,IAAI,EAAE;AAChE;","names":[]}