@hazem3500/komo 0.0.0-stage → 0.8.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/NODE.md +60 -0
  3. package/NOTICE.md +45 -0
  4. package/README.md +399 -2
  5. package/cli/agent.mjs +615 -0
  6. package/cli/config.mjs +34 -0
  7. package/cli/index.mjs +405 -0
  8. package/cli/project.mjs +136 -0
  9. package/cli/workflow.mjs +42 -0
  10. package/dist/MorphingMenu.d.ts +30 -0
  11. package/dist/PointerIcon.d.ts +2 -0
  12. package/dist/SmileIcon.d.ts +2 -0
  13. package/dist/THIRD_PARTY_NOTICES.txt +387 -0
  14. package/dist/accent-picker-HSUCA4KE.js +1 -0
  15. package/dist/accent-picker.d.ts +4 -0
  16. package/dist/accent.d.ts +2 -0
  17. package/dist/account-usage.d.ts +2 -0
  18. package/dist/action-menu.d.ts +10 -0
  19. package/dist/agent-prompt.d.ts +6 -0
  20. package/dist/agent-prompt.js +1 -0
  21. package/dist/anchors.d.ts +48 -0
  22. package/dist/api.d.ts +39 -0
  23. package/dist/approved-sites.d.ts +9 -0
  24. package/dist/card-motion.d.ts +8 -0
  25. package/dist/chunk-7YUXYVQA.js +1 -0
  26. package/dist/chunk-E5X4IQJ2.js +1 -0
  27. package/dist/chunk-I7AUKTXE.js +1 -0
  28. package/dist/chunk-RPKLGTEU.js +1 -0
  29. package/dist/chunk-Y2GRLW4Q.js +1 -0
  30. package/dist/chunk-Z2ABBESO.js +1 -0
  31. package/dist/chunk-ZUNTYWJF.js +1 -0
  32. package/dist/config.d.ts +11 -0
  33. package/dist/connect-project.d.ts +14 -0
  34. package/dist/connection-issue.d.ts +7 -0
  35. package/dist/design-export-BRBI7S3V.js +1 -0
  36. package/dist/design-export.d.ts +8 -0
  37. package/dist/design-mode-styles.d.ts +1 -0
  38. package/dist/design-mode.d.ts +19 -0
  39. package/dist/design-mode.js +1 -0
  40. package/dist/design-panel-BPVXFERR.js +1 -0
  41. package/dist/design-panel-styles.d.ts +1 -0
  42. package/dist/design-panel.d.ts +33 -0
  43. package/dist/design-tokens.d.ts +76 -0
  44. package/dist/dom.d.ts +10 -0
  45. package/dist/drawer-expand-motion.d.ts +46 -0
  46. package/dist/drawer-presence.d.ts +4 -0
  47. package/dist/edge-sidebar.d.ts +39 -0
  48. package/dist/emoji-history.d.ts +8 -0
  49. package/dist/emoji.d.ts +1 -0
  50. package/dist/floating-drag.d.ts +9 -0
  51. package/dist/icon-markup.d.ts +4 -0
  52. package/dist/index.d.ts +8 -0
  53. package/dist/index.js +1 -0
  54. package/dist/inline-text-editor-F7B6FQQ3.js +1 -0
  55. package/dist/inline-text-editor.d.ts +70 -0
  56. package/dist/lazy-accent-picker.d.ts +4 -0
  57. package/dist/lazy-toolbar.d.ts +19 -0
  58. package/dist/message-links.d.ts +2 -0
  59. package/dist/morphing-menu-styles.d.ts +1 -0
  60. package/dist/node.mjs +2454 -0
  61. package/dist/onboarding-YA63RUKK.js +1 -0
  62. package/dist/onboarding.d.ts +6 -0
  63. package/dist/optimistic.d.ts +26 -0
  64. package/dist/page.d.ts +2 -0
  65. package/dist/picker-WQN53YJ5.js +1 -0
  66. package/dist/pin-direction.d.ts +12 -0
  67. package/dist/pin-stacks.d.ts +3 -0
  68. package/dist/polling.d.ts +10 -0
  69. package/dist/postgres/001_initial.sql +128 -0
  70. package/dist/postgres/002_thread_tags.sql +1 -0
  71. package/dist/postgres/003_project_webhooks.sql +4 -0
  72. package/dist/project-management.d.ts +2 -0
  73. package/dist/react-inspector.d.ts +43 -0
  74. package/dist/react-props-panel-XSMDR2CY.js +1 -0
  75. package/dist/react-props-panel.d.ts +39 -0
  76. package/dist/react.d.ts +38 -0
  77. package/dist/react.js +1 -0
  78. package/dist/reaction-picker.d.ts +1 -0
  79. package/dist/review-layout.d.ts +23 -0
  80. package/dist/setup-browser.d.ts +1 -0
  81. package/dist/setup.d.ts +4 -0
  82. package/dist/setup.js +1 -0
  83. package/dist/shortcuts.d.ts +15 -0
  84. package/dist/static-svg.d.ts +3 -0
  85. package/dist/styles.d.ts +1 -0
  86. package/dist/tag-editor-VDPZ6CO3.js +1 -0
  87. package/dist/tag-editor.d.ts +12 -0
  88. package/dist/toolbar-runtime-MYOLQPTJ.js +1 -0
  89. package/dist/toolbar-runtime.d.ts +5 -0
  90. package/dist/types.d.ts +150 -0
  91. package/package.json +114 -4
  92. package/server/digest.ts +8 -0
  93. package/server/import-project.ts +164 -0
  94. package/server/index.ts +1565 -0
  95. package/server/migrations/0001_comments.sql +54 -0
  96. package/server/migrations/0002_identity.sql +2 -0
  97. package/server/migrations/0003_accent_color.sql +1 -0
  98. package/server/migrations/0004_workspaces.sql +46 -0
  99. package/server/migrations/0005_domains.sql +6 -0
  100. package/server/migrations/0006_member_budget.sql +16 -0
  101. package/server/migrations/0007_revisions.sql +17 -0
  102. package/server/migrations/0008_starter_comments.sql +1 -0
  103. package/server/migrations/0009_retention.sql +19 -0
  104. package/server/migrations/0010_reviewer_history.sql +1 -0
  105. package/server/migrations/0011_project_management.sql +52 -0
  106. package/server/migrations/0012_setup_sites.sql +3 -0
  107. package/server/migrations/0013_project_sites.sql +8 -0
  108. package/server/migrations/0014_project_site_removals.sql +3 -0
  109. package/server/migrations/0015_query_efficiency.sql +6 -0
  110. package/server/migrations/0016_thread_tags.sql +1 -0
  111. package/server/migrations/0017_project_webhooks.sql +4 -0
  112. package/server/node-webhooks.ts +126 -0
  113. package/server/project-management.ts +387 -0
  114. package/server/project-sites.ts +117 -0
  115. package/server/secrets.d.ts +18 -0
  116. package/server/setup-client.txt +2904 -0
  117. package/server/setup-page.ts +14 -0
  118. package/server/tsconfig.json +12 -0
  119. package/server/validation.ts +220 -0
  120. package/server/webhooks.ts +206 -0
  121. package/server/workspaces.ts +168 -0
  122. package/src/emoji.ts +7 -0
  123. package/src/page.ts +4 -0
  124. package/src/types.ts +153 -0
package/cli/index.mjs ADDED
@@ -0,0 +1,405 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile, writeFile, access, cp, rm } from "node:fs/promises";
3
+ import { resolve, dirname, basename, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { spawn } from "node:child_process";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { randomUUID, randomBytes, createHash } from "node:crypto";
8
+ import { branchName, clientModule, gitValue, repository } from "./config.mjs";
9
+
10
+ import { installAgentWorkflow } from "./workflow.mjs";
11
+ import { agentCommands, agentHelp, runAgent } from "./agent.mjs";
12
+
13
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
14
+ const cwd = process.cwd();
15
+ const args = process.argv.slice(2);
16
+ const command = args[0];
17
+ const flag = (name) => {
18
+ const i = args.indexOf(name);
19
+ return i < 0 ? undefined : args[i + 1];
20
+ };
21
+ const exists = async (path) => {
22
+ try {
23
+ await access(path);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ };
29
+ const settingsPath = resolve(cwd, ".komo/project.json");
30
+ const generatedPath = resolve(cwd, "komo.config.js");
31
+ const run = (program, argv, options = {}) =>
32
+ new Promise((resolveRun, reject) => {
33
+ const child = spawn(program, argv, {
34
+ cwd,
35
+ stdio: ["inherit", "pipe", "inherit"],
36
+ shell: false,
37
+ ...options,
38
+ });
39
+ let output = "";
40
+ child.stdout?.on("data", (chunk) => {
41
+ output += String(chunk);
42
+ process.stdout.write(chunk);
43
+ });
44
+ child.on("error", reject);
45
+ child.on("exit", (code) =>
46
+ code === 0
47
+ ? resolveRun(output)
48
+ : reject(Error(`${program} exited with ${code}`))
49
+ );
50
+ });
51
+ async function request(endpoint, path, data) {
52
+ const response = await fetch(new URL(path, endpoint), {
53
+ method: "POST",
54
+ headers: { "Content-Type": "application/json" },
55
+ body: JSON.stringify(data),
56
+ signal: AbortSignal.timeout(15000),
57
+ });
58
+ const result = await response.json();
59
+ if (!response.ok)
60
+ throw Error(result.error || `Request failed (${response.status}).`);
61
+ return result;
62
+ }
63
+ async function sync() {
64
+ const settings = JSON.parse(await readFile(settingsPath, "utf8"));
65
+ const config = { ...settings };
66
+ delete config.origin;
67
+ if (config.scope === "branch") {
68
+ config.branch = branchName(process.env, cwd);
69
+ if (!config.branch)
70
+ throw Error("Cannot detect the branch. Set KOMO_BRANCH for this build.");
71
+ }
72
+ await writeFile(generatedPath, clientModule(config));
73
+ console.log("Updated komo.config.js");
74
+ }
75
+ async function protectLocalFiles() {
76
+ const path = resolve(cwd, ".gitignore");
77
+ const existing = (await exists(path)) ? await readFile(path, "utf8") : "";
78
+ const additions = [
79
+ ".komo/owner-key",
80
+ ".komo/setup.json",
81
+ ".komo/.dev.vars",
82
+ ".komo/node.env",
83
+ ".komo/.wrangler/",
84
+ "komo.config.js",
85
+ ].filter((line) => !existing.split("\n").includes(line));
86
+ if (additions.length)
87
+ await writeFile(path, `${existing}\n# komo\n${additions.join("\n")}\n`);
88
+ }
89
+ async function deploy() {
90
+ const dir = resolve(cwd, ".komo"),
91
+ path = join(dir, "wrangler.json");
92
+ const statePath = join(dir, "deployment.json");
93
+ const state = JSON.parse(await readFile(statePath, "utf8"));
94
+ const wr = (...argv) =>
95
+ run("npx", ["--yes", "wrangler@4", ...argv, "--config", path]);
96
+ await wr("login");
97
+ let config = JSON.parse(await readFile(path, "utf8"));
98
+ if (!config.d1_databases?.length) {
99
+ await wr("d1", "create", config.name, "--binding", "DB", "--update-config");
100
+ config = JSON.parse(await readFile(path, "utf8"));
101
+ }
102
+ config.d1_databases[0].migrations_dir = "migrations";
103
+ await writeFile(path, JSON.stringify(config, null, 2));
104
+ await cp(join(packageRoot, "server/migrations"), join(dir, "migrations"), {
105
+ recursive: true,
106
+ });
107
+ await wr("d1", "migrations", "apply", "DB", "--remote");
108
+ const output = await wr("deploy");
109
+ const endpoint =
110
+ flag("--endpoint") ||
111
+ state.endpoint ||
112
+ output.match(/https:\/\/[a-z0-9.-]+\.workers\.dev\b/)?.[0];
113
+ if (!endpoint)
114
+ throw Error(
115
+ "No API URL returned. Run komo deploy --endpoint YOUR_WORKER_URL."
116
+ );
117
+ await writeFile(statePath, JSON.stringify({ ...state, endpoint }, null, 2));
118
+ console.log("Store the Google OAuth client secret:");
119
+ await wr("secret", "put", "GOOGLE_CLIENT_SECRET");
120
+ await writeFile(
121
+ settingsPath,
122
+ `${JSON.stringify(
123
+ {
124
+ endpoint,
125
+ project: state.project,
126
+ repo: state.repo,
127
+ scope: state.scope,
128
+ origin: state.origin,
129
+ },
130
+ null,
131
+ 2
132
+ )}\n`
133
+ );
134
+ await sync();
135
+ await installAgentWorkflow(cwd);
136
+ const key = await readFile(join(dir, "owner-key"), "utf8");
137
+ console.log(
138
+ `Register ${endpoint}/auth/google/callback in Google Console.\nOpen ${endpoint}/setup?project=${state.project}#${key} to claim ownership with Google.`
139
+ );
140
+ }
141
+ async function init() {
142
+ if (await exists(resolve(cwd, ".komo/deployment.json")))
143
+ throw Error("Self-host setup exists. Run komo deploy to resume.");
144
+ const previous = (await exists(settingsPath))
145
+ ? JSON.parse(await readFile(settingsPath, "utf8"))
146
+ : null;
147
+ const resuming = previous?.onboarding?.inProject === true;
148
+ if (
149
+ !resuming &&
150
+ ((await exists(settingsPath)) || (await exists(generatedPath)))
151
+ )
152
+ throw Error(
153
+ "komo is already configured. Edit .komo/project.json, then run komo sync."
154
+ );
155
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
156
+ const ask = async (text, fallback = "") => {
157
+ if (!process.stdin.isTTY) {
158
+ if (fallback) return fallback;
159
+ throw Error(
160
+ `${text} Supply the corresponding CLI flag in non-interactive mode.`
161
+ );
162
+ }
163
+ return (
164
+ (
165
+ await rl.question(`${text}${fallback ? ` [${fallback}]` : ""}: `)
166
+ ).trim() || fallback
167
+ );
168
+ };
169
+ try {
170
+ const selfHosted = args.includes("--self-host");
171
+ const nodeHosted = args.includes("--node");
172
+ if (selfHosted && nodeHosted) throw Error("Choose --node or --self-host.");
173
+ const detected = repository(gitValue(["remote", "get-url", "origin"], cwd));
174
+ const project = `komo_${randomUUID().replaceAll("-", "")}`;
175
+ const repo =
176
+ flag("--repo") || (resuming ? previous.repo : "") || detected || project;
177
+ const origin =
178
+ flag("--origin") ||
179
+ (resuming ? previous.origin : "") ||
180
+ process.env.CF_PAGES_URL ||
181
+ (process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "") ||
182
+ process.env.DEPLOY_PRIME_URL ||
183
+ "http://localhost:3000";
184
+ const parsed = new URL(origin);
185
+ if (
186
+ parsed.origin !== origin ||
187
+ !["https:", "http:"].includes(parsed.protocol) ||
188
+ (parsed.protocol === "http:" &&
189
+ !["localhost", "127.0.0.1"].includes(parsed.hostname))
190
+ )
191
+ throw Error("Use an exact HTTPS origin, or localhost.");
192
+ const origins = [
193
+ ...new Set([
194
+ origin,
195
+ ...[
196
+ process.env.CF_PAGES_URL,
197
+ process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "",
198
+ process.env.DEPLOY_PRIME_URL,
199
+ ]
200
+ .filter(Boolean)
201
+ .map((site) => new URL(site).origin),
202
+ ]),
203
+ ];
204
+ const scope = args.includes("--branch-scope")
205
+ ? "branch"
206
+ : resuming
207
+ ? previous.scope
208
+ : "project";
209
+ let config;
210
+ if (!selfHosted && !nodeHosted) {
211
+ const endpoint =
212
+ flag("--endpoint") ||
213
+ (resuming ? previous.endpoint : "") ||
214
+ "https://komo.offbr.co";
215
+ const api = new URL(endpoint);
216
+ if (
217
+ api.protocol !== "https:" &&
218
+ !["localhost", "127.0.0.1"].includes(api.hostname)
219
+ )
220
+ throw Error("The API endpoint must use HTTPS.");
221
+ const setupPath = resolve(cwd, ".komo/setup.json");
222
+ let setup =
223
+ resuming && (await exists(setupPath))
224
+ ? JSON.parse(await readFile(setupPath, "utf8"))
225
+ : null;
226
+ if (
227
+ !setup ||
228
+ setup.expiresAt <= Date.now() ||
229
+ flag("--origin") ||
230
+ flag("--repo") ||
231
+ flag("--endpoint")
232
+ ) {
233
+ setup = {
234
+ ...(await request(endpoint, "/setup/start", { repo, origins })),
235
+ expiresAt: Date.now() + 600000,
236
+ };
237
+ }
238
+ await mkdir(dirname(settingsPath), { recursive: true });
239
+ await protectLocalFiles();
240
+ await writeFile(setupPath, JSON.stringify(setup), { mode: 0o600 });
241
+ await writeFile(
242
+ settingsPath,
243
+ `${JSON.stringify({ endpoint, project: `setup_${setup.id}`, repo, scope, origin, onboarding: { inProject: true, code: setup.id, sites: origins.filter((site) => site.startsWith("https://")) } }, null, 2)}\n`
244
+ );
245
+ await sync();
246
+ await installAgentWorkflow(cwd);
247
+ console.log(
248
+ `\nMount komo in your app:\n\nimport { initKomo } from './komo.config.js';\ninitKomo();\n\nOpen ${origin} and choose Connect komo in the sidebar.\nKeep this terminal open while you start your app in another terminal.\nHosted recovery: ${setup.url}\n`
249
+ );
250
+ console.log("Waiting for your project to connect…");
251
+ const end = setup.expiresAt;
252
+ while (Date.now() < end) {
253
+ await new Promise((resolveWait) => setTimeout(resolveWait, 3000));
254
+ const result = await request(endpoint, "/setup/poll", {
255
+ id: setup.id,
256
+ secret: setup.secret,
257
+ });
258
+ if (!result.pending) {
259
+ config = {
260
+ endpoint: result.endpoint,
261
+ project: result.project,
262
+ repo: result.repo,
263
+ scope,
264
+ origin,
265
+ };
266
+ break;
267
+ }
268
+ }
269
+ if (!config)
270
+ throw Error("Sign-in timed out. Run komo init again to resume setup.");
271
+ await rm(setupPath, { force: true });
272
+ } else if (nodeHosted) {
273
+ const endpoint = flag("--endpoint") || (await ask("Public API URL"));
274
+ const api = new URL(endpoint);
275
+ if (
276
+ api.origin !== endpoint ||
277
+ (api.protocol !== "https:" &&
278
+ !["localhost", "127.0.0.1"].includes(api.hostname))
279
+ ) throw Error("Use an HTTPS API origin, or localhost.");
280
+ const previewOrigin = flag("--preview-origin");
281
+ if (previewOrigin &&
282
+ !/^https:\/\/[a-z0-9-]*\*[a-z0-9-]*\.[a-z0-9.-]+$/.test(previewOrigin))
283
+ throw Error("Use a single-label HTTPS preview wildcard.");
284
+ const googleClient =
285
+ flag("--google-client-id") || (await ask("Google OAuth client ID"));
286
+ const key = randomBytes(32).toString("hex");
287
+ const dir = resolve(cwd, ".komo");
288
+ await mkdir(dir, { recursive: true });
289
+ await protectLocalFiles();
290
+ const projectConfig = {
291
+ repo,
292
+ origins: [...new Set([...origins, ...(previewOrigin ? [previewOrigin] : [])])],
293
+ requireOwner: true,
294
+ bootstrapHash: createHash("sha256").update(key).digest("hex"),
295
+ };
296
+ await writeFile(join(dir, "owner-key"), key, { mode: 0o600 });
297
+ await writeFile(join(dir, "node.env"), [
298
+ "DATABASE_URL=REPLACE_WITH_POSTGRES_URL",
299
+ `PUBLIC_URL=${endpoint}`,
300
+ `PROJECTS=${JSON.stringify({ [project]: projectConfig })}`,
301
+ `GOOGLE_CLIENT_ID=${googleClient}`,
302
+ "GOOGLE_CLIENT_SECRET=REPLACE_WITH_GOOGLE_SECRET",
303
+ "PORT=8080",
304
+ "",
305
+ ].join("\n"), { mode: 0o600 });
306
+ config = { endpoint, project, repo, scope, origin };
307
+ await writeFile(settingsPath, `${JSON.stringify(config, null, 2)}\n`);
308
+ await sync();
309
+ await installAgentWorkflow(cwd);
310
+ console.log(
311
+ `Edit .komo/node.env with your PostgreSQL URL and Google secret.\nDeploy the Node container with those variables.\nRegister ${endpoint}/auth/google/callback in Google Console.\nOpen ${endpoint}/setup?project=${project}#${key} to claim ownership after the API is live.\n`
312
+ );
313
+ return;
314
+ } else {
315
+ if (
316
+ !(await exists(resolve(cwd, "node_modules/@hazem3500/komo/package.json")))
317
+ )
318
+ throw Error(
319
+ "Install komo in this project before self-hosting: npm install @hazem3500/komo (or install the preview tarball before publication)."
320
+ );
321
+ await protectLocalFiles();
322
+ const name = `komo-${basename(cwd)
323
+ .toLowerCase()
324
+ .replace(/[^a-z0-9-]/g, "-")
325
+ .slice(0, 32)}-${project.slice(-6)}`;
326
+ const googleClient =
327
+ flag("--google-client-id") || (await ask("Google OAuth client ID"));
328
+ const key = randomBytes(32).toString("hex");
329
+ const dir = resolve(cwd, ".komo");
330
+ await mkdir(dir, { recursive: true });
331
+ await cp(
332
+ join(packageRoot, "server/migrations"),
333
+ join(dir, "migrations"),
334
+ { recursive: true }
335
+ );
336
+ await writeFile(
337
+ join(dir, "index.ts"),
338
+ 'export { default } from "@hazem3500/komo/server";\n'
339
+ );
340
+ const wrangler = {
341
+ name,
342
+ main: "index.ts",
343
+ compatibility_date: "2026-07-02",
344
+ compatibility_flags: ["nodejs_compat"],
345
+ observability: { enabled: true },
346
+ vars: {
347
+ PROJECTS: JSON.stringify({
348
+ [project]: {
349
+ repo,
350
+ origins,
351
+ requireOwner: true,
352
+ bootstrapHash: createHash("sha256").update(key).digest("hex"),
353
+ },
354
+ }),
355
+ GOOGLE_CLIENT_ID: googleClient,
356
+ GITHUB_CLIENT_ID: "",
357
+ GITHUB_CLIENT_SECRET: "",
358
+ },
359
+ };
360
+ const path = join(dir, "wrangler.json");
361
+ await writeFile(path, JSON.stringify(wrangler, null, 2));
362
+ await writeFile(join(dir, "owner-key"), key, { mode: 0o600 });
363
+ await writeFile(
364
+ join(dir, "deployment.json"),
365
+ JSON.stringify({ project, repo, scope, origin }, null, 2)
366
+ );
367
+ await deploy();
368
+ return;
369
+ }
370
+ await mkdir(dirname(settingsPath), { recursive: true });
371
+ await writeFile(settingsPath, `${JSON.stringify(config, null, 2)}\n`);
372
+ await protectLocalFiles();
373
+ await sync();
374
+ await installAgentWorkflow(cwd);
375
+ console.log("Added the komo comment workflow to AGENTS.md");
376
+ console.log(
377
+ `\nMount after the page loads:\n\nimport { initKomo } from '@hazem3500/komo';\ninitKomo(${JSON.stringify({ ...config, origin: undefined, ...(config.scope === "branch" ? { branch: branchName(process.env, cwd) } : {}) }, null, 2)});\n\nFor automatic branch detection, use the generated komo.config.js helper and run komo sync before your build.\n`
378
+ );
379
+ } finally {
380
+ rl.close();
381
+ }
382
+ }
383
+ try {
384
+ if (args.includes("--help") || !command)
385
+ console.log(
386
+ `komo\n\n komo init Create a Google-owned hosted workspace\n komo init --self-host Deploy your own Worker and D1 database\n komo init --node Prepare a Node/PostgreSQL project\n komo deploy Resume self-hosted deployment\n komo sync Regenerate client settings; detect the current branch\n\nOptions: --origin URL --endpoint API_URL --repo OWNER/REPO --branch-scope\nSelf-host: --google-client-id ID; Node: --preview-origin HTTPS_WILDCARD\n\nComments are shared across deployments unless --branch-scope is set.${
387
+ agentHelp
388
+ }`
389
+ );
390
+ else if (agentCommands.includes(command)) await runAgent(args);
391
+ else if (command === "init") await init();
392
+ else if (command === "deploy") await deploy();
393
+ else if (command === "sync") await sync();
394
+ else throw Error(`Unknown command: ${command}`);
395
+ } catch (error) {
396
+ console.error(
397
+ agentCommands.includes(command)
398
+ ? JSON.stringify({
399
+ ok: false,
400
+ error: { message: error.message, status: error.status ?? null },
401
+ })
402
+ : error.message
403
+ );
404
+ process.exitCode = 1;
405
+ }
@@ -0,0 +1,136 @@
1
+ import { readFile, writeFile, stat } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+
4
+ export const projectHelp = `
5
+ Project commands (Google owner):
6
+ komo project info
7
+ komo project export --out comments.json
8
+ komo project import --file comments.json
9
+ komo project access --access public|private
10
+ komo project invite --email name@example.com
11
+ komo project join --invite TOKEN
12
+ komo project revoke --email name@example.com
13
+ komo project remove-member --user USER_ID
14
+ komo project clear-resolved --confirm PROJECT_KEY
15
+ komo project delete --confirm PROJECT_KEY
16
+ Exports exclude credentials. Imports preserve feedback with unverified imported identities.
17
+ `;
18
+ export async function runProject(action, flags, request, config, cwd) {
19
+ const requireValue = (name) => {
20
+ if (!flags[name]) throw Error(`Supply --${name}.`);
21
+ return flags[name];
22
+ };
23
+ if (action === "info" || action === "list") return request("project");
24
+ if (action === "export") {
25
+ const path = resolve(cwd, requireValue("out"));
26
+ const snapshot = {
27
+ format: "komo-export",
28
+ version: 1,
29
+ project: config.project,
30
+ exportedAt: new Date().toISOString(),
31
+ tables: {},
32
+ };
33
+ let revision;
34
+ for (const table of ["users", "threads", "comments", "reactions"]) {
35
+ snapshot.tables[table] = [];
36
+ let offset = 0;
37
+ do {
38
+ const page = await request(
39
+ `project/export?table=${table}&offset=${offset}${revision === undefined ? "" : `&revision=${revision}`}`
40
+ );
41
+ revision = page.revision;
42
+ snapshot.tables[table].push(...page.rows);
43
+ offset = page.next;
44
+ } while (offset !== null);
45
+ }
46
+ await writeFile(path, JSON.stringify(snapshot, null, 2), {
47
+ mode: 0o600,
48
+ flag: "wx",
49
+ });
50
+ return {
51
+ file: path,
52
+ counts: Object.fromEntries(
53
+ Object.entries(snapshot.tables).map(([key, rows]) => [key, rows.length])
54
+ ),
55
+ };
56
+ }
57
+ if (action === "import") {
58
+ const path = resolve(cwd, requireValue("file"));
59
+ if ((await stat(path)).size > 50 * 1024 * 1024)
60
+ throw Error("Import files must be under 50 MB.");
61
+ const snapshot = JSON.parse(await readFile(path, "utf8"));
62
+ if (
63
+ snapshot.format !== "komo-export" ||
64
+ snapshot.version !== 1 ||
65
+ typeof snapshot.project !== "string" ||
66
+ !snapshot.tables
67
+ )
68
+ throw Error("Use a version 1 komo export.");
69
+ let imported = 0;
70
+ for (const kind of ["users", "threads", "comments", "reactions"]) {
71
+ if (!Array.isArray(snapshot.tables[kind]))
72
+ throw Error(`Missing ${kind} table.`);
73
+ let records = [];
74
+ const send = async () => {
75
+ if (records.length) {
76
+ const result = await request("project/import", "POST", {
77
+ source: snapshot.project,
78
+ kind,
79
+ records,
80
+ });
81
+ imported += result.imported;
82
+ records = [];
83
+ }
84
+ };
85
+ for (const row of snapshot.tables[kind]) {
86
+ if (
87
+ Buffer.byteLength(
88
+ JSON.stringify({
89
+ source: snapshot.project,
90
+ kind,
91
+ records: [...records, row],
92
+ })
93
+ ) > 15000 ||
94
+ records.length >= 50
95
+ )
96
+ await send();
97
+ if (
98
+ Buffer.byteLength(
99
+ JSON.stringify({ source: snapshot.project, kind, records: [row] })
100
+ ) > 16000
101
+ )
102
+ throw Error(`An imported ${kind} record exceeds 16 KB.`);
103
+ records.push(row);
104
+ }
105
+ await send();
106
+ }
107
+ return {
108
+ imported,
109
+ note: "Imported identities are unverified. Re-running this export is idempotent.",
110
+ };
111
+ }
112
+ if (action === "access")
113
+ return request("project", "PATCH", { access: requireValue("access") });
114
+ if (action === "invite")
115
+ return request("project/invites", "POST", { email: requireValue("email") });
116
+ if (action === "revoke")
117
+ return request("project/invites", "DELETE", {
118
+ email: requireValue("email"),
119
+ });
120
+ if (action === "remove-member")
121
+ return request("project/members", "DELETE", { user: requireValue("user") });
122
+ if (action === "join")
123
+ return request("project/join", "POST", { invite: requireValue("invite") });
124
+ if (action === "delete" || action === "clear-resolved") {
125
+ if (requireValue("confirm") !== config.project)
126
+ throw Error(
127
+ "--confirm must match the project key. Export first; deletion is permanent."
128
+ );
129
+ return request(
130
+ action === "delete" ? "project" : "project/clear-resolved",
131
+ action === "delete" ? "DELETE" : "POST",
132
+ { confirm: flags.confirm }
133
+ );
134
+ }
135
+ throw Error("Unknown project action. Run komo --help.");
136
+ }
@@ -0,0 +1,42 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { agentWorkflow } from "../dist/agent-prompt.js";
4
+
5
+ const start = "<!-- komo:workflow:start -->";
6
+ const end = "<!-- komo:workflow:end -->";
7
+
8
+ export async function installAgentWorkflow(cwd) {
9
+ const path = join(cwd, "AGENTS.md");
10
+ let existing = "";
11
+ try {
12
+ existing = await readFile(path, "utf8");
13
+ } catch (error) {
14
+ if (error.code !== "ENOENT") throw error;
15
+ }
16
+ const block = `${start}
17
+ ## komo comment workflow
18
+
19
+ Use the project-local komo CLI (for example, npx --no-install komo). Run komo login if a session is needed.
20
+
21
+ ${agentWorkflow}
22
+ ${end}`;
23
+ const first = existing.indexOf(start);
24
+ const last = existing.indexOf(end);
25
+ if (
26
+ (first === -1) !== (last === -1) ||
27
+ (first !== -1 &&
28
+ (last < first ||
29
+ existing.indexOf(start, first + start.length) !== -1 ||
30
+ existing.indexOf(end, last + end.length) !== -1))
31
+ )
32
+ throw Error(
33
+ "AGENTS.md has an incomplete or duplicate komo workflow block. Repair its markers before retrying."
34
+ );
35
+ const content =
36
+ first === -1
37
+ ? `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${existing ? "\n" : ""}${block}
38
+ `
39
+ : existing.slice(0, first) + block + existing.slice(last + end.length);
40
+ if (content !== existing) await writeFile(path, content);
41
+ return { path, changed: content !== existing };
42
+ }
@@ -0,0 +1,30 @@
1
+ import { type CSSProperties, type ReactNode } from "react";
2
+ export type MenuAction = {
3
+ id: string;
4
+ label: string;
5
+ icon: ReactNode;
6
+ activeIcon?: ReactNode;
7
+ href?: string;
8
+ onSelect?: () => void;
9
+ keepOpenOnSelect?: boolean;
10
+ expandedOrder?: number;
11
+ };
12
+ export type MenuItem = MenuAction & {
13
+ children?: readonly MenuAction[];
14
+ /** Set false to keep this item in More without adding a dock shortcut. */
15
+ showInBar?: boolean;
16
+ };
17
+ export type MorphingMenuProps = {
18
+ items: readonly MenuItem[];
19
+ defaultActiveId?: string;
20
+ activeId?: string;
21
+ alignEnd?: boolean;
22
+ edge?: "top" | "bottom" | "left" | "right";
23
+ onActiveChange?: (id: string) => void;
24
+ label?: string;
25
+ moreLabel?: string;
26
+ backLabel?: string;
27
+ className?: string;
28
+ style?: CSSProperties;
29
+ };
30
+ export declare function MorphingMenu({ items, defaultActiveId, activeId, edge, alignEnd, onActiveChange, label, moreLabel, backLabel, className, style, }: MorphingMenuProps): import("react").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import type { SVGProps } from "react";
2
+ export declare function PointerIcon(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;
@@ -0,0 +1,2 @@
1
+ import type { SVGProps } from "react";
2
+ export declare function SmileIcon(props: SVGProps<SVGSVGElement>): import("react").JSX.Element;