@evandu/super-pi 0.1.132 → 0.1.135

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 (69) hide show
  1. package/dist/.super-pi-check-inputs.json +4 -0
  2. package/dist/client/.super-pi-client-sources.json +8 -8
  3. package/dist/client/assets/{CodeMirrorEditor-ktyL2PcX.js → CodeMirrorEditor-D6awRUZO.js} +2 -2
  4. package/dist/client/assets/{dist--igDAzRS.js → dist-1jSyapdW.js} +1 -1
  5. package/dist/client/assets/{dist-CPLCQDqy.js → dist-BGgidsaF.js} +1 -1
  6. package/dist/client/assets/{dist-DNZMXYNq.js → dist-BYMYC_2I.js} +1 -1
  7. package/dist/client/assets/{dist-vsMBLoxV.js → dist-BkVaPA_q.js} +1 -1
  8. package/dist/client/assets/{dist-B8OZ-n_C.js → dist-BoeM5xW7.js} +1 -1
  9. package/dist/client/assets/{dist-D_Zdnhm8.js → dist-C46474ys.js} +1 -1
  10. package/dist/client/assets/{dist-Cu75NyQ0.js → dist-CCkdCH6a.js} +1 -1
  11. package/dist/client/assets/{dist-BJi5HmH4.js → dist-CNuKauOq.js} +1 -1
  12. package/dist/client/assets/{dist-BAJCrbnX.js → dist-CubYGjAk.js} +1 -1
  13. package/dist/client/assets/{dist-8pdzh6h1.js → dist-Cwb-Lb5-.js} +1 -1
  14. package/dist/client/assets/{dist-BRjGckmQ.js → dist-D0Iw5XQA.js} +1 -1
  15. package/dist/client/assets/{dist-BjZUYxLd.js → dist-DCiuVvGU.js} +1 -1
  16. package/dist/client/assets/{dist-Hj_dWgPr.js → dist-DHBWUQhM.js} +1 -1
  17. package/dist/client/assets/{dist-CoszOk5k.js → dist-DOwrSiuY.js} +1 -1
  18. package/dist/client/assets/{dist-Bg9zLfex.js → dist-DeZsNbFx.js} +1 -1
  19. package/dist/client/assets/{dist-C6iAHGvb.js → dist-Dk3e6RfY.js} +1 -1
  20. package/dist/client/assets/{dist-Bv6P2Rcp.js → dist-DoDmoWTn.js} +1 -1
  21. package/dist/client/assets/{dist-CgmISAMr.js → dist-WkVUFX28.js} +1 -1
  22. package/dist/client/assets/{dist-DvHghuGd.js → dist-lZUWPQbT.js} +1 -1
  23. package/dist/client/assets/{dist-Zhdkfvng.js → dist-mPR8uuzk.js} +1 -1
  24. package/dist/client/assets/{dist-60usx__o.js → dist-spB2Q2CG.js} +1 -1
  25. package/dist/client/assets/{dist-CMscy2Nn.js → dist-yTTCht0J.js} +1 -1
  26. package/dist/client/assets/index-Dyta-HkQ.js +100 -0
  27. package/dist/client/assets/{index-D3MEG8uH.css → index-V93Kzott.css} +1 -1
  28. package/dist/client/index.html +2 -2
  29. package/dist/server/active-memory.d.ts +1 -0
  30. package/dist/server/active-memory.js +6 -2
  31. package/dist/server/agents.js +21 -1
  32. package/dist/server/application/feishu-service.d.ts +57 -10
  33. package/dist/server/application/feishu-service.js +118 -24
  34. package/dist/server/application/run-service.d.ts +4 -0
  35. package/dist/server/application/run-service.js +123 -88
  36. package/dist/server/application/workspace-service.d.ts +8 -4
  37. package/dist/server/application/workspace-service.js +11 -10
  38. package/dist/server/failure-text.d.ts +18 -0
  39. package/dist/server/failure-text.js +44 -0
  40. package/dist/server/feishu-cli.d.ts +18 -2
  41. package/dist/server/feishu-cli.js +46 -12
  42. package/dist/server/feishu-config.d.ts +68 -0
  43. package/dist/server/feishu-config.js +84 -0
  44. package/dist/server/feishu.d.ts +187 -0
  45. package/dist/server/feishu.js +695 -0
  46. package/dist/server/http-server.js +36 -5
  47. package/dist/server/index.js +5 -2
  48. package/dist/server/knowledge-embedding-worker.d.ts +1 -0
  49. package/dist/server/knowledge-embedding-worker.js +27 -0
  50. package/dist/server/knowledge-vectors.d.ts +5 -3
  51. package/dist/server/knowledge-vectors.js +49 -47
  52. package/dist/server/knowledge.d.ts +1 -0
  53. package/dist/server/knowledge.js +6 -2
  54. package/dist/server/persistence/sqlite-state-repository.d.ts +0 -1
  55. package/dist/server/persistence/sqlite-state-repository.js +55 -1
  56. package/dist/server/pi-session-events.js +10 -2
  57. package/dist/server/pi-session-pool.d.ts +33 -1
  58. package/dist/server/pi-session-pool.js +20 -2
  59. package/dist/server/providers.d.ts +13 -0
  60. package/dist/server/providers.js +27 -2
  61. package/dist/server/runtime.d.ts +26 -1
  62. package/dist/server/runtime.js +98 -9
  63. package/dist/server/store.d.ts +61 -0
  64. package/dist/server/store.js +112 -14
  65. package/dist/server/types.d.ts +10 -0
  66. package/dist/server/ws-server.d.ts +3 -0
  67. package/dist/server/ws-server.js +35 -6
  68. package/package.json +25 -2
  69. package/dist/client/assets/index-ChyemF2I.js +0 -100
@@ -0,0 +1,68 @@
1
+ import type { FeishuDomain } from "./feishu.js";
2
+ /**
3
+ * THE FEISHU CREDENTIALS, and where they live.
4
+ *
5
+ * WHY A FILE IN THE DATA DIRECTORY AND NOT A FIELD IN THE APP STATE. The App State is broadcast to
6
+ * every browser that connects, and it is written to disk in a form that is meant to be readable.
7
+ * A secret does not belong there - the same rule the provider registry follows, where the API key
8
+ * lives in `<dataDir>/providers.json` with restrictive permissions and the browser is told only
9
+ * that a key exists.
10
+ *
11
+ * WHY THE ENVIRONMENT OVERRIDES THE FILE. A deployment that provisions the credential through the
12
+ * environment should not have to write a file as well, and the environment is the harder thing to
13
+ * override accidentally. The file is the fallback, not the other way round.
14
+ *
15
+ * WHY THERE IS ONE SET OF CREDENTIALS FOR THE WHOLE SERVER. Feishu credentials belong to an
16
+ * application inside one enterprise, and Super Pi runs on one person's machine. A per-workspace
17
+ * credential would be a second enterprise's application, and the user of that workspace would have
18
+ * to be handed a secret they never asked for.
19
+ */
20
+ export type FeishuConfig = {
21
+ /** The application's App ID. */
22
+ appId?: string;
23
+ /** The application's App Secret. Never leaves the server. */
24
+ appSecret?: string;
25
+ /** Which Feishu the calls go to. */
26
+ domain?: FeishuDomain;
27
+ };
28
+ /** Where the credentials come from, for the panel to show. */
29
+ export type FeishuConfigSource = "environment" | "file" | "none";
30
+ export declare class FeishuConfigStore {
31
+ private readonly file;
32
+ constructor(file: string);
33
+ /**
34
+ * The credentials in force, and where they came from.
35
+ *
36
+ * The environment wins over the file, and the file is read only when the environment has not
37
+ * said anything - so a machine that provisions the credential through the environment never
38
+ * touches the file at all.
39
+ */
40
+ read(): Promise<{
41
+ config: FeishuConfig;
42
+ source: FeishuConfigSource;
43
+ }>;
44
+ /**
45
+ * The file's own contents, and only its own contents.
46
+ *
47
+ * A file that cannot be read is a file that is not there, and a file that cannot be parsed is a
48
+ * file this server did not write. Both are reported rather than thrown: a broken credential
49
+ * file must not stop the server from starting.
50
+ */
51
+ private readFile;
52
+ /**
53
+ * Writes the credentials.
54
+ *
55
+ * The write is atomic (a temporary file and a rename) so a crash cannot leave half a JSON file
56
+ * behind, and the file's mode is 0600 so that a secret is not readable by every account on the
57
+ * machine. On Windows the mode is not enforced the same way, and there is nothing this code can
58
+ * do about that.
59
+ */
60
+ write(config: FeishuConfig): Promise<void>;
61
+ }
62
+ /**
63
+ * The domain a name means, defaulting to Feishu.
64
+ *
65
+ * A name this does not recognise is Feishu rather than an error: the domain is a convenience for
66
+ * Lark users, and refusing to start over a typo in it would be worse than trying Feishu.
67
+ */
68
+ export declare function domainOf(value: string | undefined): FeishuDomain;
@@ -0,0 +1,84 @@
1
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ import { dirname } from "node:path";
4
+ export class FeishuConfigStore {
5
+ file;
6
+ constructor(file) {
7
+ this.file = file;
8
+ }
9
+ /**
10
+ * The credentials in force, and where they came from.
11
+ *
12
+ * The environment wins over the file, and the file is read only when the environment has not
13
+ * said anything - so a machine that provisions the credential through the environment never
14
+ * touches the file at all.
15
+ */
16
+ async read() {
17
+ const fromEnv = {
18
+ ...(process.env.SUPER_PI_FEISHU_APP_ID ? { appId: process.env.SUPER_PI_FEISHU_APP_ID.trim() } : {}),
19
+ ...(process.env.SUPER_PI_FEISHU_APP_SECRET ? { appSecret: process.env.SUPER_PI_FEISHU_APP_SECRET.trim() } : {}),
20
+ ...(process.env.SUPER_PI_FEISHU_DOMAIN ? { domain: domainOf(process.env.SUPER_PI_FEISHU_DOMAIN) } : {}),
21
+ };
22
+ if (fromEnv.appId && fromEnv.appSecret)
23
+ return { config: fromEnv, source: "environment" };
24
+ const stored = await this.readFile();
25
+ if (fromEnv.appId || fromEnv.appSecret)
26
+ return { config: { ...stored, ...fromEnv }, source: "environment" };
27
+ return { config: stored, source: stored.appId && stored.appSecret ? "file" : "none" };
28
+ }
29
+ /**
30
+ * The file's own contents, and only its own contents.
31
+ *
32
+ * A file that cannot be read is a file that is not there, and a file that cannot be parsed is a
33
+ * file this server did not write. Both are reported rather than thrown: a broken credential
34
+ * file must not stop the server from starting.
35
+ */
36
+ async readFile() {
37
+ try {
38
+ const raw = await readFile(this.file, "utf8");
39
+ const parsed = JSON.parse(raw);
40
+ return {
41
+ ...(typeof parsed.appId === "string" ? { appId: parsed.appId } : {}),
42
+ ...(typeof parsed.appSecret === "string" ? { appSecret: parsed.appSecret } : {}),
43
+ ...(parsed.domain ? { domain: domainOf(parsed.domain) } : {}),
44
+ };
45
+ }
46
+ catch {
47
+ return {};
48
+ }
49
+ }
50
+ /**
51
+ * Writes the credentials.
52
+ *
53
+ * The write is atomic (a temporary file and a rename) so a crash cannot leave half a JSON file
54
+ * behind, and the file's mode is 0600 so that a secret is not readable by every account on the
55
+ * machine. On Windows the mode is not enforced the same way, and there is nothing this code can
56
+ * do about that.
57
+ */
58
+ async write(config) {
59
+ await mkdir(dirname(this.file), { recursive: true });
60
+ const temporary = `${this.file}.${randomUUID()}.tmp`;
61
+ const value = {
62
+ ...(config.appId ? { appId: config.appId } : {}),
63
+ ...(config.appSecret ? { appSecret: config.appSecret } : {}),
64
+ ...(config.domain ? { domain: config.domain } : {}),
65
+ };
66
+ try {
67
+ await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
68
+ await rename(temporary, this.file);
69
+ }
70
+ catch (error) {
71
+ await writeFile(temporary, "{}", { mode: 0o600 }).catch(() => undefined);
72
+ throw error;
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * The domain a name means, defaulting to Feishu.
78
+ *
79
+ * A name this does not recognise is Feishu rather than an error: the domain is a convenience for
80
+ * Lark users, and refusing to start over a typo in it would be worse than trying Feishu.
81
+ */
82
+ export function domainOf(value) {
83
+ return String(value ?? "").trim().toLowerCase() === "lark" ? "lark" : "feishu";
84
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * THE FEISHU CLIENT: everything Super Pi knows how to do with a cloud document on Feishu.
3
+ *
4
+ * WHY THIS LAYER EXISTS AT ALL. Feishu's document API is not "read a file" and "write a file". A
5
+ * document is a TREE OF BLOCKS, the API refuses to create a document with content in it, and the
6
+ * official way to put Markdown into a document is a three-step dance (convert to blocks, then
7
+ * insert them as nested blocks under the page block). An agent that had to know that would be
8
+ * writing protocol details into every prompt. So the protocol lives here, once, and what is left
9
+ * above it is "read this document" and "write this document".
10
+ *
11
+ * WHY AN HTTP CLIENT AND NOT THE OFFICIAL SDK. The official SDK (`@larksuiteoapi/node-sdk`) is a
12
+ * good library, but it is a dependency of the PACKAGE, and this package's whole point is that it
13
+ * installs and runs. Every call it makes is a plain HTTP call with a bearer token, so the four
14
+ * endpoints this feature needs are four functions - and the credential handling (a token that
15
+ * expires, a token that has to be refreshed, a rate limit that has to be waited out) is the part
16
+ * that is worth writing carefully and testing on its own.
17
+ *
18
+ * WHAT THE IDENTITY IS. Everything here runs as the APPLICATION (`tenant_access_token`), never as
19
+ * a person. That is the identity that needs no OAuth dance and no browser, and it is also the
20
+ * identity that can only touch the documents the application has been made a collaborator on.
21
+ * See `docs/FEISHU.md` for the setup that implies, because it is the one thing a user must do
22
+ * before any of this works.
23
+ */
24
+ /** The tool that reads a Feishu document. */
25
+ export declare const FEISHU_READ_TOOL_NAME = "feishu_doc_read";
26
+ /** The tool that writes a Feishu document. */
27
+ export declare const FEISHU_WRITE_TOOL_NAME = "feishu_doc_write";
28
+ /** Which flavour of Feishu we talk to. The API paths differ, the shapes do not. */
29
+ export type FeishuDomain = "feishu" | "lark";
30
+ /** A block in a Feishu document. Only the fields this code reads are named. */
31
+ export type FeishuBlock = {
32
+ block_id: string;
33
+ parent_id?: string;
34
+ children?: string[];
35
+ block_type: number;
36
+ [key: string]: unknown;
37
+ };
38
+ /**
39
+ * A REFUSAL FROM FEISHU, with the code kept alongside the message.
40
+ *
41
+ * The code is kept because the message Feishu sends is written for a developer reading a log,
42
+ * and the part a person needs - "add this application as a collaborator", "open this permission
43
+ * point" - is not in it.
44
+ */
45
+ export declare class FeishuError extends Error {
46
+ readonly code: number;
47
+ readonly hint?: string | undefined;
48
+ constructor(code: number, message: string, hint?: string | undefined);
49
+ }
50
+ /**
51
+ * The message a refusal is shown with: what Feishu said, and what to do about it.
52
+ */
53
+ export declare function describeFeishuError(code: number, message: string): string;
54
+ /**
55
+ * WHAT A URL OR A TOKEN NAMES.
56
+ *
57
+ * `kind` is "docx" for a document and "wiki" for a knowledge-base node. The distinction is not
58
+ * cosmetic: a wiki URL's token is NOT the document id, and passing it as one produces a document
59
+ * that does not exist. A wiki link has to be resolved through the wiki API first.
60
+ */
61
+ export type FeishuTarget = {
62
+ kind: "docx" | "wiki";
63
+ token: string;
64
+ /** The tenant host the link came from, when it came from a link. Used to build a URL for a new document. */
65
+ host?: string;
66
+ };
67
+ /**
68
+ * Reads a document reference: a full URL, or a bare token.
69
+ *
70
+ * WHY BARE TOKENS ARE ACCEPTED. A person reading a document's URL sees the token in it, and an
71
+ * agent that has been handed `doxcn...` should not have to be told to wrap it in a URL first.
72
+ */
73
+ export declare function parseFeishuTarget(input: string): FeishuTarget;
74
+ /**
75
+ * Renders a document's blocks as Markdown.
76
+ *
77
+ * WHY MARKDOWN AND NOT THE RAW TEXT. `raw_content` gives the words without the shape: a heading
78
+ * arrives as a heading's text, a list arrives as separate lines, and a reader cannot tell them
79
+ * apart. The block tree is the document, and Markdown is the closest plain-text shape this side of
80
+ * the product already understands.
81
+ */
82
+ export declare function blocksToMarkdown(blocks: FeishuBlock[]): string;
83
+ /** The client the service drives. Every call goes through here. */
84
+ export type FeishuClientOptions = {
85
+ appId: string;
86
+ appSecret: string;
87
+ domain?: FeishuDomain;
88
+ /** Injected in tests, so the suite never touches the network. */
89
+ fetchImpl?: typeof fetch;
90
+ };
91
+ export declare class FeishuClient {
92
+ private readonly options;
93
+ private token?;
94
+ /** Whether the newer OAuth token endpoint works for this application. */
95
+ private useLegacyAuth;
96
+ /** The write queue. One write at a time, spaced by the interval the API allows. */
97
+ private writeChain;
98
+ private lastWriteAt;
99
+ constructor(options: FeishuClientOptions);
100
+ private get apiBase();
101
+ private get accountsBase();
102
+ private get fetchImpl();
103
+ /**
104
+ * THE ACCESS TOKEN, fetched once and reused.
105
+ *
106
+ * The lifetime is two hours and the API says to believe the number it returns rather than a
107
+ * hardcoded one, so the expiry comes from the response and the token is renewed a couple of
108
+ * minutes before it runs out.
109
+ */
110
+ accessToken(force?: boolean): Promise<string>;
111
+ /**
112
+ * The current OAuth endpoint: `POST https://accounts.feishu.cn/oauth/v3/token`.
113
+ *
114
+ * It is the one the documentation recommends. If it refuses - an older application, a tenant
115
+ * that has not been migrated - the legacy endpoint is tried instead, and the client remembers
116
+ * so the failing call is not made again on every token refresh.
117
+ */
118
+ private fetchOAuthToken;
119
+ /**
120
+ * The historical endpoint: `POST /open-apis/auth/v3/tenant_access_token/internal`.
121
+ *
122
+ * It is documented as superseded, but it is the endpoint every 自建应用 has worked with for
123
+ * years, and a fallback that costs one request is cheaper than a feature that does not run.
124
+ */
125
+ private fetchLegacyToken;
126
+ /**
127
+ * ONE CALL, with the bearer token attached.
128
+ *
129
+ * A 429 is waited out rather than reported: the API tells us how many seconds until the window
130
+ * resets, and a write that gives up because it was 200ms early is a write that did not happen.
131
+ */
132
+ private request;
133
+ /**
134
+ * Runs one write, spaced from the last one.
135
+ */
136
+ private withWriteSlot;
137
+ /** The basic information about a document: its title and its version. */
138
+ documentMeta(documentId: string): Promise<{
139
+ documentId: string;
140
+ title?: string;
141
+ revisionId?: number;
142
+ }>;
143
+ /** Every block of a document, across every page. */
144
+ documentBlocks(documentId: string): Promise<FeishuBlock[]>;
145
+ /** The plain text of a document, without any structure. */
146
+ rawContent(documentId: string): Promise<string>;
147
+ /** The blocks directly under the page block - the document's top level. */
148
+ rootChildren(documentId: string): Promise<string[]>;
149
+ /** Creates an empty document. The content goes in afterwards, because the API has no other way. */
150
+ createDocument(title: string, folderToken?: string): Promise<{
151
+ documentId: string;
152
+ revisionId?: number;
153
+ }>;
154
+ /** Resolves a knowledge-base node to the document behind it. */
155
+ wikiNode(token: string): Promise<{
156
+ objToken: string;
157
+ objType: string;
158
+ title?: string;
159
+ }>;
160
+ /**
161
+ * Turns Markdown into the blocks that represent it.
162
+ *
163
+ * The API will not create a document with content in it, and building the blocks by hand would
164
+ * mean reimplementing the conversion Feishu already does - tables, code fences, todos and all.
165
+ */
166
+ convertMarkdown(markdown: string): Promise<{
167
+ blocks: FeishuBlock[];
168
+ firstLevelBlockIds: string[];
169
+ }>;
170
+ /**
171
+ * Inserts blocks under the page block.
172
+ *
173
+ * THE CHUNKING IS NOT AN OPTIMISATION. The API accepts at most 1000 blocks per call, and the
174
+ * blocks of one subtree cannot be split across calls - so the top-level blocks are packed into
175
+ * batches that each fit, and a document of any length goes in.
176
+ */
177
+ insertBlocks(documentId: string, blocks: FeishuBlock[], firstLevelBlockIds: string[], index?: number): Promise<number | undefined>;
178
+ /** Deletes a range of the page block's children. The range is left-closed and right-open. */
179
+ deleteChildren(documentId: string, startIndex: number, endIndex: number): Promise<number | undefined>;
180
+ }
181
+ /**
182
+ * Removes the read-only merge information from a table block.
183
+ *
184
+ * The API rejects a table block that carries `merge_info`, and the conversion endpoint hands it
185
+ * back with that field on it. The documentation says to strip it, so it is stripped.
186
+ */
187
+ export declare function stripTableMergeInfo(block: FeishuBlock): FeishuBlock;