soulnet-dsh 0.1.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/cordis.patch.yml +40 -0
  4. package/lib/client.js +5427 -0
  5. package/lib/client.js.map +1 -0
  6. package/lib/commands.js +94 -0
  7. package/lib/commands.js.map +1 -0
  8. package/lib/events-Cz7g9aXF.js +134 -0
  9. package/lib/events-Cz7g9aXF.js.map +1 -0
  10. package/lib/events-DHPK3pow.js +134 -0
  11. package/lib/events-DHPK3pow.js.map +1 -0
  12. package/lib/events-DViXNRAZ.js +9 -0
  13. package/lib/events-DViXNRAZ.js.map +1 -0
  14. package/lib/index.js +2547 -0
  15. package/lib/index.js.map +1 -0
  16. package/lib/policy-HQKLctKr.js +96 -0
  17. package/lib/policy-HQKLctKr.js.map +1 -0
  18. package/lib/send-BL_Yr40O.js +33 -0
  19. package/lib/send-BL_Yr40O.js.map +1 -0
  20. package/lib/send-DcYQC5CC.js +127 -0
  21. package/lib/send-DcYQC5CC.js.map +1 -0
  22. package/lib/sessions.js +1197 -0
  23. package/lib/sessions.js.map +1 -0
  24. package/lib/tools.js +342 -0
  25. package/lib/tools.js.map +1 -0
  26. package/lib/types/alter-state.d.ts +185 -0
  27. package/lib/types/api/index.d.ts +52 -0
  28. package/lib/types/client/A2ANode.d.ts +4 -0
  29. package/lib/types/client/AlterPane.d.ts +9 -0
  30. package/lib/types/client/ChatPane.d.ts +13 -0
  31. package/lib/types/client/DraftCard.d.ts +11 -0
  32. package/lib/types/client/FriendComposer.d.ts +20 -0
  33. package/lib/types/client/FriendList.d.ts +12 -0
  34. package/lib/types/client/FriendPane.d.ts +13 -0
  35. package/lib/types/client/Inbox.d.ts +9 -0
  36. package/lib/types/client/InboxOverlay.d.ts +8 -0
  37. package/lib/types/client/Onboarding.d.ts +4 -0
  38. package/lib/types/client/SettingsSection.d.ts +29 -0
  39. package/lib/types/client/SidebarEntry.d.ts +9 -0
  40. package/lib/types/client/SoulmirrorPage.d.ts +12 -0
  41. package/lib/types/client/SoulmirrorView.d.ts +5 -0
  42. package/lib/types/client/a2a-node.d.ts +46 -0
  43. package/lib/types/client/alter-ui.d.ts +35 -0
  44. package/lib/types/client/api.d.ts +424 -0
  45. package/lib/types/client/inbox-state.d.ts +57 -0
  46. package/lib/types/client/index.d.ts +30 -0
  47. package/lib/types/client/locales.d.ts +239 -0
  48. package/lib/types/client/page-state.d.ts +116 -0
  49. package/lib/types/client/page-store.d.ts +96 -0
  50. package/lib/types/client/panel-store.d.ts +30 -0
  51. package/lib/types/client/styles.d.ts +2 -0
  52. package/lib/types/commands/index.d.ts +24 -0
  53. package/lib/types/drafts.d.ts +50 -0
  54. package/lib/types/events.d.ts +61 -0
  55. package/lib/types/friend-settings.d.ts +41 -0
  56. package/lib/types/index.d.ts +43 -0
  57. package/lib/types/network/fake.d.ts +10 -0
  58. package/lib/types/network/jsonrpc.d.ts +52 -0
  59. package/lib/types/network/send.d.ts +19 -0
  60. package/lib/types/network/soulnet.d.ts +123 -0
  61. package/lib/types/network/types.d.ts +203 -0
  62. package/lib/types/persona.d.ts +45 -0
  63. package/lib/types/policy.d.ts +105 -0
  64. package/lib/types/sessions/index.d.ts +182 -0
  65. package/lib/types/settings.d.ts +58 -0
  66. package/lib/types/tools/define.d.ts +35 -0
  67. package/lib/types/tools/index.d.ts +57 -0
  68. package/lib/types-zwvjR1n5.js +29 -0
  69. package/lib/types-zwvjR1n5.js.map +1 -0
  70. package/package.json +133 -0
  71. package/presets/soulmirror-chat/agent.cordis.yml +24 -0
  72. package/presets/soulmirror-chat/preset.yml +3 -0
@@ -0,0 +1,41 @@
1
+ import { type ReplyTier } from './policy.ts';
2
+ export declare const FRIEND_SETTINGS_FILE = "dsh-friends.json";
3
+ export declare const PROTOCOL_FILE = "protocol.md";
4
+ export interface FriendSettings {
5
+ readonly tier?: ReplyTier;
6
+ }
7
+ /** In-memory copy of dsh-friends.json with write-through. */
8
+ export declare class FriendSettingsStore {
9
+ private readonly path;
10
+ private data;
11
+ private loaded;
12
+ constructor(path: string);
13
+ static at(a2aDir: string): FriendSettingsStore;
14
+ load(): Promise<void>;
15
+ get isLoaded(): boolean;
16
+ get(fp: string): FriendSettings;
17
+ /** Tier of a friend, or `fallback` (the global default) when none is stored. */
18
+ tier(fp: string, fallback: ReplyTier): ReplyTier;
19
+ /** Every stored entry (fp → settings). */
20
+ all(): Readonly<Record<string, FriendSettings>>;
21
+ /** Patch one friend's settings; an explicit `tier: undefined` clears the stored tier (back to the global default). */
22
+ set(fp: string, patch: {
23
+ tier?: ReplyTier | undefined;
24
+ }): Promise<FriendSettings>;
25
+ remove(fp: string): Promise<void>;
26
+ }
27
+ /**
28
+ * The global diplomacy protocol file. `read()` is synchronous and cached by
29
+ * mtime so a prompt-variable provider can call it on every assembly; `write()`
30
+ * replaces the file (and the cache).
31
+ */
32
+ export declare class ProtocolFile {
33
+ readonly path: string;
34
+ private cache;
35
+ constructor(path: string);
36
+ static at(a2aDir: string): ProtocolFile;
37
+ /** Current text; `''` when the file does not exist yet (the peer writes it with the identity). */
38
+ read(): string;
39
+ exists(): boolean;
40
+ write(text: string): void;
41
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * soulnet-dsh — host root entry = the `soulmirror-network` plugin.
3
+ *
4
+ * Provides `ctx.soulmirror` (NetworkClient: the `soulnet` light peer by
5
+ * default, the in-memory fake on request) and `ctx.soulmirrorHome`, registers
6
+ * the `soulmirror` user-settings namespace and mounts the browser-facing HTTP
7
+ * API (./api). The bare package name is also what dsh's client-module scan keys
8
+ * on, so this entry carries the browser bundle declaration (package.json
9
+ * `dsh.client` + the `./client` export).
10
+ *
11
+ * Host side rule: NO @deepseek-ai VALUE imports into the harness instance
12
+ * (types only; the one vendored library we do import, schemastery, is inlined).
13
+ * A linked (`dsh plugin add ./packages/dsh`) package resolves bare specifiers
14
+ * from its own real path, where the harness packages are not installed; and a
15
+ * second copy of cordis/dsh-tools would be a different runtime instance anyway.
16
+ */
17
+ import type { Context } from '@deepseek-ai/cordis';
18
+ import type { NetworkClient } from './network/types.ts';
19
+ import { type SoulmirrorSettings } from './settings.ts';
20
+ export type * from './network/types.ts';
21
+ export type * from './events.ts';
22
+ export { SOULMIRROR_PLUGIN, RELAY_FORM } from './events.ts';
23
+ export { SETTINGS_NAMESPACE } from './settings.ts';
24
+ export type { SoulmirrorSettings } from './settings.ts';
25
+ /** Live view of the `soulmirror` settings (the alter fields apply without a restart). */
26
+ export interface SoulmirrorConfig {
27
+ current(): SoulmirrorSettings;
28
+ }
29
+ declare module '@deepseek-ai/cordis' {
30
+ interface Context {
31
+ /** SoulMirror network client (identity / card / friends / send / subscribe). */
32
+ soulmirror: NetworkClient;
33
+ /** Backend data directory (`a2a/` underneath: identity.json, friends.yaml, conversations/ …; same layout as ~/.soulmirror/a2a). */
34
+ soulmirrorHome: string;
35
+ /** Live settings: `defaultTier` / `autoReplyPerHour` / `directSend` are read per use; connection fields apply on reload. */
36
+ soulmirrorConfig: SoulmirrorConfig;
37
+ }
38
+ }
39
+ /** Composition entry config; every field is also a user setting (namespace `soulmirror`). */
40
+ export type Config = Partial<SoulmirrorSettings>;
41
+ export declare const name = "soulmirror-network";
42
+ export declare const inject: string[];
43
+ export declare function apply(ctx: Context, config?: Config): void;
@@ -0,0 +1,10 @@
1
+ import { type Friend, type NetworkClient, type PendingRequest } from './types.ts';
2
+ export declare const FAKE_FRIENDS: readonly Friend[];
3
+ export declare const FAKE_PENDING: readonly PendingRequest[];
4
+ export interface FakeOptions {
5
+ /** Delay before the canned inbound message fires after the first subscribe (ms); negative = never. */
6
+ readonly firstInboundDelayMs?: number;
7
+ /** Start without an identity (first-run onboarding path). Default: identity present. */
8
+ readonly noIdentity?: boolean;
9
+ }
10
+ export declare function createFakeNetworkClient(options?: FakeOptions): NetworkClient;
@@ -0,0 +1,52 @@
1
+ import type { Readable, Writable } from 'node:stream';
2
+ export interface JsonRpcErrorShape {
3
+ readonly code: number;
4
+ readonly message: string;
5
+ readonly data?: unknown;
6
+ }
7
+ /** A JSON-RPC error response, or a transport failure (code -32099 family). */
8
+ export declare class JsonRpcError extends Error {
9
+ readonly code: number;
10
+ readonly data?: unknown | undefined;
11
+ readonly name = "JsonRpcError";
12
+ constructor(message: string, code: number, data?: unknown | undefined);
13
+ }
14
+ /** Transport-level code: the endpoint closed before the response arrived. */
15
+ export declare const JSONRPC_CLOSED = -32099;
16
+ /** Transport-level code: no response within the request timeout. */
17
+ export declare const JSONRPC_TIMEOUT = -32098;
18
+ export interface JsonRpcNotification {
19
+ readonly method: string;
20
+ readonly params: unknown;
21
+ }
22
+ export interface JsonRpcEndpointOptions {
23
+ /** Default per-request timeout in ms (0 = none). */
24
+ readonly timeoutMs?: number;
25
+ readonly onNotification?: (notification: JsonRpcNotification) => void;
26
+ /** Unparseable or malformed inbound lines (never thrown). */
27
+ readonly onProtocolError?: (error: Error, line: string) => void;
28
+ /** The read side ended (EOF/error); every pending request is rejected first. */
29
+ readonly onClose?: (error?: Error) => void;
30
+ }
31
+ export declare class JsonRpcEndpoint {
32
+ private readonly input;
33
+ private readonly output;
34
+ private readonly options;
35
+ private readonly pending;
36
+ private readonly reader;
37
+ private nextId;
38
+ private closed;
39
+ constructor(input: Readable, output: Writable, options?: JsonRpcEndpointOptions);
40
+ get isClosed(): boolean;
41
+ /** Send a request and await its result; rejects with {@link JsonRpcError}. */
42
+ request(method: string, params?: unknown, options?: {
43
+ timeoutMs?: number;
44
+ signal?: AbortSignal;
45
+ }): Promise<unknown>;
46
+ /** Fire-and-forget request without an id (no response expected). */
47
+ notify(method: string, params?: unknown): void;
48
+ /** Reject every pending request and stop reading. Idempotent. */
49
+ close(error?: Error): void;
50
+ private write;
51
+ private handleLine;
52
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Send through the peer and read the archived entry back (shared by the
3
+ * browser API's direct send and the alter's `soulmirror_send_message` tool):
4
+ * the peer answers `seq` / `status`; the archive line carries the exact `ts`
5
+ * and `auto` flag, which the SoulMirror page needs to reconcile its
6
+ * optimistic bubble or to paint the alter's send. Falls back to a constructed
7
+ * entry when the read-back misses.
8
+ */
9
+ import type { Fingerprint } from '../events.ts';
10
+ import type { ConversationEntry, NetworkClient, SendOptions } from './types.ts';
11
+ export interface SentEntry {
12
+ readonly entry: ConversationEntry;
13
+ readonly receipt: {
14
+ id: string;
15
+ seq?: number;
16
+ status: string;
17
+ };
18
+ }
19
+ export declare function sendAndArchive(client: NetworkClient, fp: Fingerprint, body: string, options?: SendOptions): Promise<SentEntry>;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * `soulnet` backend: spawns the soulnet light peer (Go, ../cmd/soulnet) and
3
+ * drives it over line-delimited JSON-RPC 2.0 on stdio (protocol: cmd/soulnet/
4
+ * README.md, `initialize.protocol === "soulnet/1"`).
5
+ *
6
+ * - request/response by id, notifications → `subscribe` listeners;
7
+ * - the process is restarted with exponential backoff when it dies;
8
+ * - `dispose()` sends `shutdown`, then kills the process if it lingers;
9
+ * - calls issued while the peer is (re)starting wait for it up to the request
10
+ * timeout instead of failing immediately.
11
+ *
12
+ * Binary lookup (`resolveSoulnetBinary` / `locateSoulnetBinary`), in order:
13
+ * 1. the explicit `peerBinary` setting (absolute path, or a bare name looked up on PATH);
14
+ * 2. the platform package `soulnet-peer-<os>-<arch>` installed next to
15
+ * this plugin as an optional dependency (`require.resolve('<pkg>/package.json')`
16
+ * from this file and from its realpath, so pnpm's symlinked virtual store and
17
+ * the hoisted layout both work) -> `<pkg>/bin/soulnet[.exe]`;
18
+ * 3. `soulnet` on PATH;
19
+ * 4. `<plugin dir>/bin/soulnet[.exe]` (a hand-dropped binary for development).
20
+ * The winner and its source are reported in `BackendStatus.binary` / `binarySource`.
21
+ */
22
+ import { type ChildProcess } from 'node:child_process';
23
+ import { type Friend, type NetworkClient, type PendingRequest } from './types.ts';
24
+ export declare const DEFAULT_RELAY = "https://relay.startupworld.cn";
25
+ export declare const SOULNET_PROTOCOL = "soulnet/1";
26
+ export type SoulnetLogger = (level: 'info' | 'warn' | 'error', message: string) => void;
27
+ export interface SoulnetSpawnRequest {
28
+ readonly binary: string;
29
+ readonly args: readonly string[];
30
+ }
31
+ export interface SoulnetClientOptions {
32
+ /** Data directory passed as `--home` (`a2a/` lives underneath). */
33
+ readonly home: string;
34
+ /** Relay URL passed as `--relay` (only used when the identity is created). */
35
+ readonly relay?: string;
36
+ /** Create the identity with this name on first start (`initialize {name}`); empty = wait for the host. */
37
+ readonly displayName?: string;
38
+ /** Explicit binary path; when absent {@link resolveSoulnetBinary} runs. */
39
+ readonly peerBinary?: string;
40
+ /** Per-request timeout (default 30 s; `message.send` with the relay down can take a while). */
41
+ readonly requestTimeoutMs?: number;
42
+ /** Restart backoff (ms). Defaults: 500 → ×2 → max 30 000. */
43
+ readonly backoff?: {
44
+ readonly initialMs?: number;
45
+ readonly maxMs?: number;
46
+ readonly factor?: number;
47
+ };
48
+ /** Test seam: replace `child_process.spawn`. */
49
+ readonly spawn?: (request: SoulnetSpawnRequest) => ChildProcess;
50
+ readonly logger?: SoulnetLogger;
51
+ /** Extra env for the child (merged over process.env). */
52
+ readonly env?: Record<string, string>;
53
+ }
54
+ /** Default home: `$SOULNET_HOME`, else `~/.soulnet` (same rule as the binary itself). */
55
+ export declare function defaultSoulnetHome(env?: NodeJS.ProcessEnv): string;
56
+ /** Where the binary came from (reported in `BackendStatus.binarySource`). */
57
+ export type SoulnetBinarySource = 'setting' | 'platform-package' | 'path' | 'plugin-bin';
58
+ export interface SoulnetBinaryLocation {
59
+ readonly path: string;
60
+ readonly source: SoulnetBinarySource;
61
+ }
62
+ /** npm scope of the platform packages that ship the binary. */
63
+ /** Prefix of the per-platform binary packages on npm (`soulnet-peer-<os>-<arch>`). */
64
+ export declare const PLATFORM_PACKAGE_PREFIX = "soulnet-peer-";
65
+ /** The `<os>-<arch>` pairs a platform package exists for (must match dsh/packages/soulnet-*). */
66
+ export declare const PLATFORM_PACKAGE_TARGETS: readonly string[];
67
+ /** `soulnet-peer-<os>-<arch>` for a supported pair, else `undefined`. */
68
+ export declare function platformPackageName(platform?: NodeJS.Platform, arch?: string): string | undefined;
69
+ export interface ResolveSoulnetBinaryOptions {
70
+ /** `process.arch` by default. */
71
+ readonly arch?: string;
72
+ /** Test seam: package name -> installed package directory (default: `require.resolve` next to this file). */
73
+ readonly resolvePackageDir?: (name: string) => string | undefined;
74
+ }
75
+ /**
76
+ * Find the `soulnet` binary (order documented at the top of this file) and say
77
+ * where it came from. `undefined` when nothing was found (the caller reports a
78
+ * clear error).
79
+ */
80
+ export declare function locateSoulnetBinary(explicit: string | undefined, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, options?: ResolveSoulnetBinaryOptions): SoulnetBinaryLocation | undefined;
81
+ /** Path-only form of {@link locateSoulnetBinary}. */
82
+ export declare function resolveSoulnetBinary(explicit: string | undefined, env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, options?: ResolveSoulnetBinaryOptions): string | undefined;
83
+ interface WireCard {
84
+ name?: string;
85
+ }
86
+ interface WireMessage {
87
+ id?: string;
88
+ from?: string;
89
+ to?: string;
90
+ ts?: string;
91
+ type?: string;
92
+ body?: string;
93
+ auto?: boolean;
94
+ artifact_name?: string;
95
+ card?: WireCard;
96
+ }
97
+ interface WireFriend {
98
+ fingerprint?: string;
99
+ note?: string;
100
+ protocol?: string;
101
+ card?: WireCard;
102
+ added_at?: string;
103
+ count?: number;
104
+ unread?: number;
105
+ last?: WireMessage;
106
+ typing?: boolean;
107
+ }
108
+ interface WirePending {
109
+ id?: string;
110
+ peer?: string;
111
+ incoming?: WireMessage;
112
+ created_at?: string;
113
+ }
114
+ export declare function friendFromWire(w: WireFriend): Friend;
115
+ export declare function pendingFromWire(w: WirePending): PendingRequest;
116
+ /**
117
+ * Create the soulnet-backed NetworkClient. The process is spawned lazily on
118
+ * the first call or on `start()`; `dispose()` stops it.
119
+ */
120
+ export declare function createSoulnetNetworkClient(options: SoulnetClientOptions): NetworkClient & {
121
+ start(): void;
122
+ };
123
+ export {};
@@ -0,0 +1,203 @@
1
+ /**
2
+ * NetworkClient — the ONE interface every SoulMirror network backend
3
+ * implements (architecture spec §2). Host plugins only ever see this
4
+ * interface. Two implementations ship:
5
+ *
6
+ * - `soulnet` (default): spawns the `soulnet` light peer (Go, ../cmd/soulnet)
7
+ * and talks line-delimited JSON-RPC 2.0 over its stdio (./soulnet.ts);
8
+ * - `fake`: an in-memory backend for tests and UI work (./fake.ts).
9
+ */
10
+ import type { A2AMessageId, Fingerprint } from '../events.ts';
11
+ export type BackendKind = 'fake' | 'soulnet';
12
+ export interface Identity {
13
+ readonly fp: Fingerprint;
14
+ readonly name: string;
15
+ /** Card URI (`soulmirror://card?...`) others paste to add us. */
16
+ readonly cardUri: string;
17
+ /** ISO timestamp of identity creation when known. */
18
+ readonly createdAt?: string;
19
+ }
20
+ export interface Friend {
21
+ readonly fp: Fingerprint;
22
+ /** Display name: the local note when set, else the name on the peer's card. */
23
+ readonly name: string;
24
+ /** Local note (SoulMirror "note"); undefined when none was set. */
25
+ readonly remark?: string;
26
+ /** Name the peer declared on its card. */
27
+ readonly cardName?: string;
28
+ /** Per-friend diplomacy protocol override (friends.yaml `protocol`); undefined/empty = the global protocol applies. */
29
+ readonly protocol?: string;
30
+ readonly online?: boolean;
31
+ /** Unread inbound messages (sent by the peer after our read cursor). */
32
+ readonly unread: number;
33
+ /** Total archived conversation entries. */
34
+ readonly count: number;
35
+ /** Timestamp (ms) of the last archived entry, when any. */
36
+ readonly lastTs?: number;
37
+ /** Preview of the last archived entry body. */
38
+ readonly lastBody?: string;
39
+ /** The peer is signalling "busy"/typing right now. */
40
+ readonly typing?: boolean;
41
+ readonly addedAt?: string;
42
+ }
43
+ export interface PendingRequest {
44
+ /** Request id (= the request message id); what `friends.accept/reject` take. */
45
+ readonly id: string;
46
+ readonly fp: Fingerprint;
47
+ /** Name on the requester's card. */
48
+ readonly name: string;
49
+ /** Greeting text sent with the request. */
50
+ readonly greeting: string;
51
+ readonly createdAt?: string;
52
+ }
53
+ export interface InboundMessage {
54
+ readonly id: A2AMessageId;
55
+ readonly from: Fingerprint;
56
+ /** Display name of the sender as known when the message arrived. */
57
+ readonly name: string;
58
+ readonly body: string;
59
+ /** Unix epoch ms. */
60
+ readonly ts: number;
61
+ /** 1-based line number in the peer's conversation archive, when archived. */
62
+ readonly seq?: number;
63
+ readonly auto?: true;
64
+ /** A2A message type (`text`, `app_share`, …). */
65
+ readonly type?: string;
66
+ /** Absolute path of an attachment written to disk, when any. */
67
+ readonly artifactPath?: string;
68
+ readonly artifactName?: string;
69
+ }
70
+ export interface ConversationEntry {
71
+ readonly seq: number;
72
+ readonly dir: 'in' | 'out';
73
+ readonly id: A2AMessageId;
74
+ readonly body: string;
75
+ readonly ts: number;
76
+ readonly type?: string;
77
+ readonly auto?: true;
78
+ /** Outbound only: `sent` | `queued` | `error`. */
79
+ readonly status?: string;
80
+ readonly artifactName?: string;
81
+ }
82
+ /** Optional parts of one send. */
83
+ export interface SendOptions {
84
+ /** Absolute path of one local file to attach. */
85
+ readonly file?: string;
86
+ /** Mark the message as the alter's automatic reply (A2A `auto` flag; the receiver's loop guard). */
87
+ readonly auto?: boolean;
88
+ }
89
+ export interface SendReceipt {
90
+ readonly id: A2AMessageId;
91
+ readonly seq?: number;
92
+ /** `sent`, or `queued` when the relay was unreachable (outbox retry). */
93
+ readonly status: string;
94
+ }
95
+ export type BackendState = 'starting' | 'ready' | 'restarting' | 'stopped' | 'error';
96
+ export interface BackendStatus {
97
+ readonly backend: BackendKind;
98
+ readonly state: BackendState;
99
+ readonly pid?: number;
100
+ readonly restarts: number;
101
+ readonly lastError?: string;
102
+ readonly relay?: string;
103
+ readonly home?: string;
104
+ readonly protocol?: string;
105
+ readonly version?: string;
106
+ /** The `soulnet` executable the backend spawned (soulnet backend only). */
107
+ readonly binary?: string;
108
+ /** How it was found: `setting` | `platform-package` | `path` | `plugin-bin`. */
109
+ readonly binarySource?: string;
110
+ }
111
+ export type NetworkEvent = {
112
+ readonly kind: 'message';
113
+ readonly message: InboundMessage;
114
+ } | {
115
+ readonly kind: 'typing';
116
+ readonly fp: Fingerprint;
117
+ readonly on: boolean;
118
+ } | {
119
+ readonly kind: 'friend_request';
120
+ readonly request: PendingRequest;
121
+ } | {
122
+ readonly kind: 'friend_accept';
123
+ readonly friend: Friend;
124
+ } | {
125
+ readonly kind: 'presence';
126
+ readonly fp: Fingerprint;
127
+ readonly online: boolean;
128
+ } | {
129
+ readonly kind: 'status';
130
+ readonly status: BackendStatus;
131
+ };
132
+ /** Error raised by a backend call; `code` follows the soulnet JSON-RPC table (cmd/soulnet/README.md). */
133
+ export declare class NetworkError extends Error {
134
+ readonly code: number;
135
+ readonly data?: unknown | undefined;
136
+ readonly name = "NetworkError";
137
+ constructor(message: string, code: number, data?: unknown | undefined);
138
+ }
139
+ /** soulnet application error codes (branch on these, never on message text). */
140
+ export declare const NetworkErrorCode: {
141
+ readonly noIdentity: -32001;
142
+ readonly notFriend: -32002;
143
+ readonly identityExists: -32003;
144
+ readonly notFound: -32004;
145
+ readonly badCard: -32005;
146
+ readonly network: -32006;
147
+ readonly badFile: -32007;
148
+ readonly noProfile: -32008;
149
+ /** Plugin-side: the backend process is not running / could not be reached. */
150
+ readonly peerUnavailable: -32099;
151
+ };
152
+ export interface NetworkClient {
153
+ readonly backend: BackendKind;
154
+ status(): BackendStatus;
155
+ /** `undefined` until an identity was created (first run). */
156
+ identity(): Promise<Identity | undefined>;
157
+ createIdentity(name: string): Promise<Identity>;
158
+ /** Own card URI (`soulmirror://card?...`). */
159
+ card(): Promise<string>;
160
+ parseCard(uri: string): Promise<{
161
+ fp: Fingerprint;
162
+ name: string;
163
+ uri: string;
164
+ }>;
165
+ readonly friends: {
166
+ list(): Promise<readonly Friend[]>;
167
+ pending(): Promise<readonly PendingRequest[]>;
168
+ /** Send a friend request from a card URI; `note` is both the local note and the greeting. */
169
+ add(cardUri: string, note?: string): Promise<Friend>;
170
+ accept(requestId: string, note?: string): Promise<Friend>;
171
+ reject(requestId: string): Promise<void>;
172
+ /** Change the local note and/or the per-friend protocol override (`protocol: ''` clears it). */
173
+ set(fp: Fingerprint, patch: {
174
+ remark?: string;
175
+ protocol?: string;
176
+ }): Promise<Friend>;
177
+ remove(fp: Fingerprint): Promise<void>;
178
+ /** A friend's card URI (from the card snapshot), e.g. to forward it. Not a friend → NetworkErrorCode.notFriend. */
179
+ card(fp: Fingerprint): Promise<{
180
+ fp: Fingerprint;
181
+ name: string;
182
+ uri: string;
183
+ }>;
184
+ };
185
+ send(to: Fingerprint, body: string, options?: SendOptions): Promise<SendReceipt>;
186
+ typing(to: Fingerprint, on: boolean): Promise<void>;
187
+ conversation(fp: Fingerprint, options?: {
188
+ since?: number;
189
+ limit?: number;
190
+ }): Promise<{
191
+ entries: readonly ConversationEntry[];
192
+ typing: boolean;
193
+ }>;
194
+ markRead(fp: Fingerprint, seq: number): Promise<void>;
195
+ presence(fps: readonly Fingerprint[]): Promise<Record<string, boolean>>;
196
+ subscribe(listener: (event: NetworkEvent) => void): () => void;
197
+ /** Stop the backend (clean `shutdown` for the peer process). Idempotent. */
198
+ dispose(): Promise<void>;
199
+ /** Test/dev seam: make the backend deliver an inbound message now. Absent on real backends. */
200
+ readonly debug?: {
201
+ inject(from: Fingerprint, body: string): void;
202
+ };
203
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The alter's persona (P3, reshaped in P4 for ONE alter session): the
3
+ * system-prompt section the sessions plugin registers in the AGENT scope of
4
+ * the alter session (`agentCtx.systemPrompt.section(...)` inside the agent
5
+ * factory's `setup(agentCtx)`), shadowing the preset's `deployment:persona`,
6
+ * and the prompt VARIABLES it references.
7
+ *
8
+ * Why an agent-scoped section with variables, and not an edited preset file
9
+ * or an injected context message:
10
+ * - the section is re-registered by `setup` on every create AND resume, so
11
+ * it survives a dsh restart; the system prompt is assembled for every
12
+ * step, so it survives compaction;
13
+ * - `{{variable}}` groups resolve at render time against the registered
14
+ * variable PROVIDERS, evaluated per assembly, so an edit of protocol.md,
15
+ * of a friend's override / tier, or a new friend reaches the very next
16
+ * turn without a restart (dsh-system-prompt renders strictly: every group
17
+ * below must have a registered provider that returns a string — see
18
+ * `PERSONA_VARIABLES`);
19
+ * - the per-TURN friend context (`trigger_*`) is resolved from the session
20
+ * log at assembly time: the friend whose mail woke this turn, or "none"
21
+ * for an owner instruction (the owner may name any friend; the roster
22
+ * carries every friend's fingerprint, tier and override).
23
+ */
24
+ /** Section name of dsh-system-prompt's persona slot (`PERSONA_SECTION` in @deepseek-ai/dsh-system-prompt). */
25
+ export declare const PERSONA_SECTION = "deployment:persona";
26
+ /** Order of that slot (`PERSONA_ORDER`). */
27
+ export declare const PERSONA_ORDER = 0;
28
+ /** The variables the template references; each has an agent-scoped provider. */
29
+ export declare const PERSONA_VARIABLES: {
30
+ readonly owner: "soulmirror_owner";
31
+ /** Roster of every friend: name, fingerprint, tier, protocol override. */
32
+ readonly friends: "soulmirror_friends";
33
+ /** The friend whose mail woke THIS turn (name), or "none" (owner instruction / unknown). */
34
+ readonly triggerFriend: "soulmirror_trigger_friend";
35
+ readonly triggerFriendFp: "soulmirror_trigger_friend_fp";
36
+ readonly triggerTier: "soulmirror_trigger_tier";
37
+ readonly triggerProtocol: "soulmirror_trigger_friend_protocol";
38
+ readonly protocol: "soulmirror_protocol";
39
+ /** Pending drafts awaiting the owner's review (count + one line each). */
40
+ readonly drafts: "soulmirror_pending_drafts";
41
+ };
42
+ /** Persona text of the alter session; `{{…}}` groups are the variables above. */
43
+ export declare const PERSONA_TEMPLATE: string;
44
+ /** Text shown when a variable has no content (the renderer refuses empty values). */
45
+ export declare const PERSONA_NONE = "(none)";
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Reply policy of the alter (P3, reshaped in P4 for the single alter session)
3
+ * — pure, no I/O, unit-tested in test/policy.test.ts:
4
+ *
5
+ * - `ReplyTier` per friend: `notify` (inbound mail is appended to the alter
6
+ * session, no turn), `draft` (a turn is woken; the alter's reply is stored
7
+ * as a PENDING DRAFT the owner reviews on the SoulMirror page), `auto` (a
8
+ * turn is woken; the reply is sent without review, rate-limited per
9
+ * friend per hour).
10
+ * - `routeInbound()` — what to do with one inbound mail: append only, or
11
+ * append AND wake the alter. The loop guard lives here: mail flagged
12
+ * `auto` (another alter's automatic reply) never wakes a turn, and mail
13
+ * from a non-friend never does either.
14
+ * - `sendGate()` — whether `soulmirror_send_message` may send now or must
15
+ * queue a draft. Owner-initiated turns send freely (SoulMirror rule:
16
+ * friend messages on the owner's instruction are direct; only tasks /
17
+ * money need confirmation). Inbound-triggered turns send freely only to
18
+ * the friend who wrote, in the `auto` tier and under the hourly cap;
19
+ * everything else becomes a draft for the owner. There is no dsh
20
+ * approval panel in this path any more (P4).
21
+ * - `HourlyWindow` — the per-friend sliding-window counter behind the cap.
22
+ */
23
+ export type ReplyTier = 'notify' | 'draft' | 'auto';
24
+ export declare const REPLY_TIERS: readonly ReplyTier[];
25
+ export declare const DEFAULT_REPLY_TIER: ReplyTier;
26
+ export declare const DEFAULT_AUTO_REPLY_PER_HOUR = 20;
27
+ export declare function isReplyTier(value: unknown): value is ReplyTier;
28
+ export declare function normalizeTier(value: unknown, fallback?: ReplyTier): ReplyTier;
29
+ /**
30
+ * What woke the current turn of the alter session:
31
+ * - `owner`: the owner typed an instruction (a `user/message` with
32
+ * `source.kind === 'user'`) — from the SoulMirror page composer or dsh's
33
+ * own input bar. `fp` is never set: an instruction that names a friend
34
+ * is still the owner speaking (the persona resolves the friend as "none");
35
+ * - `inbound`: mail from a friend woke the turn; `fp` / `name` identify
36
+ * that friend (the per-friend tier and protocol override are resolved from
37
+ * it, per turn);
38
+ * - `inbound-auto`: that mail was itself flagged `auto` (should never wake a
39
+ * turn — kept for the belt-and-braces branch in the gate);
40
+ * - `unknown`: no user message in the current turn could be attributed (a
41
+ * tool call from another session, a steer, a resumed turn …).
42
+ */
43
+ export interface TurnTrigger {
44
+ readonly kind: 'owner' | 'inbound' | 'inbound-auto' | 'unknown';
45
+ /** Fingerprint of the friend whose mail woke the turn (inbound kinds only). */
46
+ readonly fp?: string;
47
+ /** Display name of that friend at delivery time. */
48
+ readonly name?: string;
49
+ /** A2A id of that mail. */
50
+ readonly messageId?: string;
51
+ }
52
+ export declare const UNKNOWN_TRIGGER: TurnTrigger;
53
+ export interface InboundRoute {
54
+ /** Wake a model turn (the mail is delivered through the agent inbox) or append only. */
55
+ readonly action: 'wake' | 'append';
56
+ /** Why it was not woken (logged). */
57
+ readonly reason?: 'tier-notify' | 'loop-guard-auto' | 'not-a-friend';
58
+ }
59
+ export interface RouteInboundInput {
60
+ readonly tier: ReplyTier;
61
+ /** The mail carries the A2A `auto` flag (another alter's automatic reply). */
62
+ readonly auto: boolean;
63
+ /** The sender is in the friend list (the peer only archives friend mail, but the session layer may lag). */
64
+ readonly isFriend: boolean;
65
+ }
66
+ export declare function routeInbound(input: RouteInboundInput): InboundRoute;
67
+ export type SendGateReason = 'owner-initiated' | 'auto-tier' | 'draft-tier' | 'notify-tier' | 'rate-limited' | 'loop-guard-auto' | 'unknown-trigger' | 'other-friend';
68
+ export type SendDecision =
69
+ /** Send now; `auto` says whether to flag the wire message as an automatic reply. */
70
+ {
71
+ readonly kind: 'allow';
72
+ readonly auto: boolean;
73
+ readonly reason: 'owner-initiated' | 'auto-tier';
74
+ }
75
+ /** Do not send: store a pending draft for the owner to review on the page. */
76
+ | {
77
+ readonly kind: 'draft';
78
+ readonly reason: Exclude<SendGateReason, 'owner-initiated' | 'auto-tier'>;
79
+ };
80
+ export interface SendGateInput {
81
+ readonly trigger: TurnTrigger;
82
+ /** The target friend of this send. */
83
+ readonly target: string;
84
+ /** Effective reply tier of the TARGET friend. */
85
+ readonly tier: ReplyTier;
86
+ /** Automatic replies already sent to the target in the current hour window. */
87
+ readonly autoSentInWindow: number;
88
+ /** `autoReplyPerHour` (<= 0 disables automatic sends entirely). */
89
+ readonly limit: number;
90
+ }
91
+ export declare function sendGate(input: SendGateInput): SendDecision;
92
+ export declare const HOUR_MS = 3600000;
93
+ /** Sliding one-hour window of timestamps per key (friend fingerprint). */
94
+ export declare class HourlyWindow {
95
+ private readonly windowMs;
96
+ private readonly hits;
97
+ constructor(windowMs?: number);
98
+ /** Timestamps still inside the window for `key`, oldest first. */
99
+ count(key: string, now?: number): number;
100
+ /** Record one hit for `key`; returns the new count. */
101
+ record(key: string, now?: number): number;
102
+ /** Milliseconds until the oldest hit leaves the window (0 when none). */
103
+ retryAfter(key: string, now?: number): number;
104
+ private prune;
105
+ }