knowingdog 0.0.0-stage → 0.2.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.
package/knowing.mjs ADDED
@@ -0,0 +1,383 @@
1
+ #!/usr/bin/env node
2
+ import { randomUUID, randomBytes, createHash } from "node:crypto";
3
+ import { readFile, writeFile, mkdir, realpath, lstat } from "node:fs/promises";
4
+ import { resolve, dirname, relative, sep, join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { createInterface } from "node:readline";
7
+ import { execFileSync } from "node:child_process";
8
+ import { pathToFileURL } from "node:url";
9
+
10
+ export function spaceEndpoint(value) {
11
+ const url = new URL(value);
12
+ if (
13
+ url.username ||
14
+ url.password ||
15
+ url.search ||
16
+ url.hash ||
17
+ (url.protocol !== "https:" &&
18
+ !(
19
+ url.protocol === "http:" &&
20
+ ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)
21
+ ))
22
+ )
23
+ throw new Error("Use an HTTPS space URL (or localhost for development).");
24
+ const match = url.pathname.match(/^\/s\/([a-z0-9-]+)\/?$/);
25
+ if (!match)
26
+ throw new Error(
27
+ "Use the default space URL, https://your-knowing-host/s/space-slug.",
28
+ );
29
+ return new URL(`/api/knowing/spaces/${match[1]}/mcp`, url.origin);
30
+ }
31
+ const publishingTools = new Set([
32
+ "get_publishing_info",
33
+ "sync_space",
34
+ "publish_release",
35
+ ]);
36
+ // knowing-skills.mjs saves `knowing login` customer tokens per site origin,
37
+ // and the last login's site as default-site.json.
38
+ async function savedConfiguration(configDir, name) {
39
+ try {
40
+ return JSON.parse(
41
+ await readFile(
42
+ join(
43
+ configDir
44
+ ? resolve(configDir)
45
+ : join(homedir(), ".config", "knowing-skills"),
46
+ name,
47
+ ),
48
+ "utf8",
49
+ ),
50
+ );
51
+ } catch (error) {
52
+ if (error.code !== "ENOENT") throw error;
53
+ return {};
54
+ }
55
+ }
56
+ async function savedCustomerToken(space, configDir) {
57
+ const { token } = await savedConfiguration(
58
+ configDir,
59
+ `${createHash("sha256").update(new URL(space).origin).digest("hex")}.json`,
60
+ );
61
+ if (token?.startsWith("knc_")) return token;
62
+ if (token)
63
+ process.stderr.write(
64
+ "Run knowing login again to use your Knowing account here.\n",
65
+ );
66
+ return undefined;
67
+ }
68
+ function post(endpoint, message, token) {
69
+ return fetch(endpoint, {
70
+ method: "POST",
71
+ redirect: "error",
72
+ headers: {
73
+ "Content-Type": "application/json",
74
+ Accept: "application/json, text/event-stream",
75
+ "MCP-Protocol-Version": "2025-11-25",
76
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
77
+ },
78
+ body: JSON.stringify(message),
79
+ signal: AbortSignal.timeout(290000),
80
+ });
81
+ }
82
+ export async function rpc(endpoint, message, token) {
83
+ const response = await post(endpoint, message, token);
84
+ if (response.status === 202) return null;
85
+ if (!response.ok)
86
+ throw new Error(
87
+ `Knowing request failed (${response.status}). No automatic retry was made.`,
88
+ );
89
+ const result = await response.json();
90
+ if (result.error) throw new Error(result.error.message);
91
+ return result;
92
+ }
93
+ const call = async (endpoint, name, args, token) => {
94
+ const response = await rpc(
95
+ endpoint,
96
+ {
97
+ jsonrpc: "2.0",
98
+ id: randomUUID(),
99
+ method: "tools/call",
100
+ params: { name, arguments: args },
101
+ },
102
+ token,
103
+ );
104
+ if (response.result.isError)
105
+ throw new Error(
106
+ response.result.content.map((item) => item.text).join("\n"),
107
+ );
108
+ return response.result.structuredContent;
109
+ };
110
+ const option = (args, name) => {
111
+ const index = args.indexOf(`--${name}`);
112
+ if (index < 0) return undefined;
113
+ if (!args[index + 1] || args[index + 1].startsWith("--"))
114
+ throw new Error(`--${name} requires a value.`);
115
+ return args[index + 1];
116
+ };
117
+ export function workflows(space, digest, branch) {
118
+ spaceEndpoint(space);
119
+ if (!/^[0-9a-f]{64}$/.test(digest)) throw new Error("Invalid CLI digest.");
120
+ if (!branch || branch.includes("\n"))
121
+ throw new Error("Invalid published branch.");
122
+ const setup = ` - uses: actions/checkout@v4\n with:\n persist-credentials: false\n - uses: actions/setup-node@v4\n with:\n node-version: '24'\n - name: Verify pinned Knowing CLI\n run: |\n echo '${digest} .github/knowing.mjs' | sha256sum --check\n cp .github/knowing.mjs "$RUNNER_TEMP/knowing.mjs"\n`;
123
+ const common = `permissions:\n contents: read\nconcurrency:\n group: knowing-${"${{ github.workflow }}"}-${"${{ github.ref }}"}\n cancel-in-progress: false\njobs:\n knowing:\n runs-on: ubuntu-latest\n steps:\n${setup}`;
124
+ return {
125
+ "knowing-docs.yml": `name: Knowing docs and agent\non:\n workflow_dispatch:\n inputs:\n question:\n description: 'Optional question for the space agent (uses space AI usage)'\n required: false\n type: string\n${common} - name: Read docs or ask the agent\n env:\n KNOWING_SPACE: ${JSON.stringify(space)}\n QUESTION: ${"${{ inputs.question }}"}\n run: |\n if [ -n "$QUESTION" ]; then\n node "$RUNNER_TEMP/knowing.mjs" ask --space "$KNOWING_SPACE" --question "$QUESTION" > knowing-result.json\n else\n node "$RUNNER_TEMP/knowing.mjs" docs --space "$KNOWING_SPACE" > knowing-result.json\n fi\n - uses: actions/upload-artifact@v4\n with:\n name: knowing-result\n path: knowing-result.json\n`,
126
+ "knowing-publish.yml": `name: Publish Knowing space\non:\n push:\n branches: [${JSON.stringify(branch)}]\n workflow_dispatch:\n inputs:\n publish_release:\n description: 'Publish the source-defined release at this commit'\n required: false\n default: false\n type: boolean\n${common} - name: Publish existing repository pages\n env:\n KNOWING_SPACE: ${JSON.stringify(space)}\n KNOWING_TOKEN: ${"${{ secrets.KNOWING_PUBLISH_TOKEN }}"}\n PUBLISH_RELEASE: ${"${{ inputs.publish_release }}"}\n run: |\n if [ "$PUBLISH_RELEASE" = "true" ]; then\n node "$RUNNER_TEMP/knowing.mjs" publish --space "$KNOWING_SPACE" --branch "$GITHUB_REF_NAME" --commit "$GITHUB_SHA"\n else\n node "$RUNNER_TEMP/knowing.mjs" sync --space "$KNOWING_SPACE"\n fi\n`,
127
+ };
128
+ }
129
+ export async function writeNewFile(root, file, content) {
130
+ const base = await realpath(root);
131
+ const target = resolve(base, file);
132
+ if (!relative(base, target) || relative(base, target).startsWith(`..${sep}`))
133
+ throw new Error("File must stay inside the repository.");
134
+ let path = base;
135
+ for (const part of relative(base, dirname(target))
136
+ .split(sep)
137
+ .filter(Boolean)) {
138
+ path = resolve(path, part);
139
+ await mkdir(path).catch((error) => {
140
+ if (error.code !== "EEXIST") throw error;
141
+ });
142
+ if ((await lstat(path)).isSymbolicLink())
143
+ throw new Error("Refusing to write through a symbolic link.");
144
+ }
145
+ await writeFile(target, content, { flag: "wx", mode: 0o644 });
146
+ }
147
+ function github(args, input) {
148
+ return execFileSync("gh", args, {
149
+ encoding: "utf8",
150
+ ...(input ? { input } : {}),
151
+ stdio: ["pipe", "pipe", "pipe"],
152
+ });
153
+ }
154
+ function repositoryName(url) {
155
+ return url.match(/github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/)?.[1];
156
+ }
157
+ export async function main(args = process.argv.slice(2)) {
158
+ const command = args[0];
159
+ if (!command || command === "help" || args.includes("--help")) {
160
+ process.stdout.write(
161
+ "Knowing: docs, search, ask, sync, publish, init-ci, mcp, mcp-config\nUse --space https://your-knowing-host/s/your-space. Reading sends your saved knowing login, or KNOWING_CUSTOMER_TOKEN to the KNOWING_SITE origin. Publishing uses KNOWING_TOKEN from the environment.\n",
162
+ );
163
+ return;
164
+ }
165
+ const space = option(args, "space") ?? process.env.KNOWING_SPACE;
166
+ if (!space) throw new Error("--space is required.");
167
+ const endpoint = spaceEndpoint(space);
168
+ const token = process.env.KNOWING_TOKEN;
169
+ const configDir = option(args, "config-dir");
170
+ // KNOWING_CUSTOMER_TOKEN is account-wide, so it only goes to a configured
171
+ // site, never to whatever origin --space names.
172
+ const customerToken = async () => {
173
+ const environment = process.env.KNOWING_CUSTOMER_TOKEN;
174
+ const configured = [
175
+ process.env.KNOWING_SITE,
176
+ process.env.KNOWING_SPACE,
177
+ (await savedConfiguration(configDir, "default-site.json")).origin,
178
+ ].filter((value) => value && URL.canParse(value));
179
+ if (
180
+ environment &&
181
+ configured.some((value) => new URL(value).origin === endpoint.origin)
182
+ )
183
+ return environment;
184
+ const saved = await savedCustomerToken(space, configDir);
185
+ if (environment && !saved)
186
+ process.stderr.write(
187
+ `KNOWING_CUSTOMER_TOKEN was not sent to ${endpoint.origin}: it only goes to your Knowing site, set by KNOWING_SITE or your last knowing login.\n`,
188
+ );
189
+ return saved;
190
+ };
191
+ if (command === "mcp") {
192
+ const customer = await customerToken();
193
+ const lines = createInterface({
194
+ input: process.stdin,
195
+ crlfDelay: Infinity,
196
+ });
197
+ for await (const line of lines) {
198
+ if (line.length > 32000) {
199
+ process.stderr.write("MCP message too large.\n");
200
+ continue;
201
+ }
202
+ let message;
203
+ try {
204
+ message = JSON.parse(line);
205
+ } catch {
206
+ process.stdout.write(
207
+ JSON.stringify({
208
+ jsonrpc: "2.0",
209
+ id: null,
210
+ error: { code: -32700, message: "Parse error" },
211
+ }) + "\n",
212
+ );
213
+ continue;
214
+ }
215
+ try {
216
+ const name =
217
+ message?.method === "tools/call" ? message.params?.name : undefined;
218
+ // Each call carries the one token its tool accepts; discovery lists publishing tools when KNOWING_TOKEN is set.
219
+ const credential = name
220
+ ? publishingTools.has(name)
221
+ ? token
222
+ : customer
223
+ : (token ?? customer);
224
+ const response = await post(endpoint, message, credential);
225
+ if (response.status === 202) continue;
226
+ const reply = await response.json();
227
+ if (reply?.jsonrpc !== "2.0") throw new Error("Invalid response");
228
+ process.stdout.write(JSON.stringify(reply) + "\n");
229
+ } catch {
230
+ if (message?.id !== undefined)
231
+ process.stdout.write(
232
+ JSON.stringify({
233
+ jsonrpc: "2.0",
234
+ id: message.id,
235
+ error: {
236
+ code: -32603,
237
+ message: "Knowing request failed. No automatic retry was made.",
238
+ },
239
+ }) + "\n",
240
+ );
241
+ }
242
+ }
243
+ return;
244
+ }
245
+ if (command === "mcp-config") {
246
+ process.stdout.write(
247
+ JSON.stringify(
248
+ {
249
+ mcpServers: {
250
+ [endpoint.pathname.split("/").at(-2)]: {
251
+ command: "node",
252
+ args: [
253
+ resolve(process.argv[1]),
254
+ "mcp",
255
+ "--space",
256
+ space,
257
+ ...(configDir ? ["--config-dir", resolve(configDir)] : []),
258
+ ],
259
+ },
260
+ },
261
+ },
262
+ null,
263
+ 2,
264
+ ) + "\n",
265
+ );
266
+ process.stderr.write(
267
+ `This server signs in with your saved knowing login for ${endpoint.origin}. Elsewhere, set KNOWING_CUSTOMER_TOKEN and KNOWING_SITE=${endpoint.origin} (or KNOWING_TOKEN for publishing) in your MCP client's environment or secret settings. Never put a token in the URL or this configuration.\n`,
268
+ );
269
+ return;
270
+ }
271
+ if (command === "init-ci") {
272
+ if (endpoint.protocol !== "https:")
273
+ throw new Error("GitHub Actions requires your hosted HTTPS Knowing URL.");
274
+ const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
275
+ encoding: "utf8",
276
+ }).trim();
277
+ const remote = execFileSync("git", ["remote", "get-url", "origin"], {
278
+ encoding: "utf8",
279
+ }).trim();
280
+ const repository = repositoryName(remote);
281
+ if (!repository)
282
+ throw new Error("The repository must have a GitHub origin.");
283
+ const info = await call(endpoint, "get_publishing_info", {}, token);
284
+ if (repository.toLowerCase() !== info.repository.toLowerCase())
285
+ throw new Error(
286
+ "This repository is not the space’s connected repository.",
287
+ );
288
+ const script = await readFile(process.argv[1], "utf8");
289
+ const digest = createHash("sha256").update(script).digest("hex");
290
+ const files = workflows(space, digest, info.branch);
291
+ files["../knowing.mjs"] = script;
292
+ for (const name of Object.keys(files)) {
293
+ try {
294
+ await lstat(resolve(root, ".github/workflows", name));
295
+ throw new Error(
296
+ `${name} already exists. Review it before replacing it.`,
297
+ );
298
+ } catch (error) {
299
+ if (error.code !== "ENOENT") throw error;
300
+ }
301
+ }
302
+ for (const [name, content] of Object.entries(files))
303
+ await writeNewFile(root, `.github/workflows/${name}`, content);
304
+ process.stdout.write(
305
+ "Created two workflows and the pinned .github/knowing.mjs CLI.\n",
306
+ );
307
+ if (args.includes("--set-secret")) {
308
+ if (!token)
309
+ throw new Error("KNOWING_TOKEN is required to set the secret.");
310
+ github(
311
+ ["secret", "set", "KNOWING_PUBLISH_TOKEN", "--repo", repository],
312
+ token,
313
+ );
314
+ process.stdout.write("Set KNOWING_PUBLISH_TOKEN in GitHub.\n");
315
+ }
316
+ try {
317
+ const secrets = JSON.parse(
318
+ github(["secret", "list", "--repo", repository, "--json", "name"]),
319
+ );
320
+ process.stdout.write(
321
+ secrets.some((secret) => secret.name === "KNOWING_PUBLISH_TOKEN")
322
+ ? "[done] Publishing secret exists.\n"
323
+ : "[todo] Set KNOWING_PUBLISH_TOKEN; run init-ci with --set-secret on a fresh setup or gh secret set KNOWING_PUBLISH_TOKEN.\n",
324
+ );
325
+ } catch {
326
+ process.stdout.write(
327
+ "[todo] Sign into gh and check the KNOWING_PUBLISH_TOKEN repository secret.\n",
328
+ );
329
+ }
330
+ process.stdout.write(
331
+ "[todo] Review, commit and push the workflows.\n[info] Workflows need only contents: read. They do not open pull requests. Publishing token expires after 90 days; rotate it in Channels and replace the secret.\n",
332
+ );
333
+ return;
334
+ }
335
+ const repository = process.env.GITHUB_REPOSITORY;
336
+ let name, input;
337
+ if (command === "docs") {
338
+ name = option(args, "page") ? "read_page" : "list_pages";
339
+ input = option(args, "page") ? { page: option(args, "page") } : {};
340
+ } else if (command === "search") {
341
+ name = "search_docs";
342
+ input = { query: option(args, "query") };
343
+ } else if (command === "ask") {
344
+ name = "ask_agent";
345
+ input = {
346
+ question: option(args, "question"),
347
+ requestId: randomUUID(),
348
+ visitorToken: randomBytes(32).toString("base64url"),
349
+ };
350
+ } else if (command === "sync") {
351
+ name = "sync_space";
352
+ input = repository ? { repository } : {};
353
+ } else if (command === "publish") {
354
+ name = "publish_release";
355
+ input = {
356
+ branch: option(args, "branch"),
357
+ commit: option(args, "commit"),
358
+ version: option(args, "version"),
359
+ license: option(args, "license"),
360
+ ...(repository ? { repository } : {}),
361
+ };
362
+ } else throw new Error(`Unknown command: ${command}`);
363
+ process.stdout.write(
364
+ JSON.stringify(
365
+ await call(
366
+ endpoint,
367
+ name,
368
+ input,
369
+ publishingTools.has(name) ? token : await customerToken(),
370
+ ),
371
+ null,
372
+ 2,
373
+ ) + "\n",
374
+ );
375
+ }
376
+ if (
377
+ process.argv[1] &&
378
+ import.meta.url === pathToFileURL(await realpath(process.argv[1])).href
379
+ )
380
+ main().catch((error) => {
381
+ process.stderr.write(`${error.message}\n`);
382
+ process.exitCode = 1;
383
+ });
package/package.json CHANGED
@@ -1,6 +1,23 @@
1
1
  {
2
2
  "name": "knowingdog",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Install source-defined Knowing skills and agents and configure GitHub Actions.",
5
+ "type": "module",
6
+ "bin": {
7
+ "knowing": "knowing-cli.mjs"
8
+ },
9
+ "files": [
10
+ "knowing-cli.mjs",
11
+ "knowing-skills.mjs",
12
+ "knowing.mjs"
13
+ ],
14
+ "engines": {
15
+ "node": ">=24"
16
+ },
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/BrickspaceLab/knowing.git",
21
+ "directory": "scripts"
22
+ }
23
+ }