@hazem3500/komo 0.0.0-stage → 0.9.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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/NODE.md +80 -0
  3. package/NOTICE.md +45 -0
  4. package/README.md +411 -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-F5UI2NP7.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-I7AUKTXE.js +1 -0
  27. package/dist/chunk-JQVBZ2BB.js +1 -0
  28. package/dist/chunk-Q7WSWLMV.js +1 -0
  29. package/dist/chunk-Y2GRLW4Q.js +1 -0
  30. package/dist/chunk-YZP3KX4Y.js +1 -0
  31. package/dist/chunk-Z2ABBESO.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-comment-QPTW66JO.js +1 -0
  36. package/dist/design-comment.d.ts +42 -0
  37. package/dist/design-export-BRBI7S3V.js +1 -0
  38. package/dist/design-export.d.ts +8 -0
  39. package/dist/design-mode-styles.d.ts +1 -0
  40. package/dist/design-mode.d.ts +19 -0
  41. package/dist/design-mode.js +1 -0
  42. package/dist/design-panel-BPVXFERR.js +1 -0
  43. package/dist/design-panel-styles.d.ts +1 -0
  44. package/dist/design-panel.d.ts +33 -0
  45. package/dist/design-tokens.d.ts +76 -0
  46. package/dist/dom.d.ts +20 -0
  47. package/dist/drawer-expand-motion.d.ts +46 -0
  48. package/dist/drawer-presence.d.ts +4 -0
  49. package/dist/edge-sidebar.d.ts +39 -0
  50. package/dist/emoji-history.d.ts +8 -0
  51. package/dist/emoji.d.ts +1 -0
  52. package/dist/floating-drag.d.ts +9 -0
  53. package/dist/icon-markup.d.ts +4 -0
  54. package/dist/index.d.ts +8 -0
  55. package/dist/index.js +1 -0
  56. package/dist/inline-text-editor-F7B6FQQ3.js +1 -0
  57. package/dist/inline-text-editor.d.ts +70 -0
  58. package/dist/lazy-accent-picker.d.ts +4 -0
  59. package/dist/lazy-toolbar.d.ts +19 -0
  60. package/dist/message-links.d.ts +2 -0
  61. package/dist/morphing-menu-styles.d.ts +1 -0
  62. package/dist/node.mjs +2607 -0
  63. package/dist/onboarding-NO3RQWR7.js +1 -0
  64. package/dist/onboarding.d.ts +6 -0
  65. package/dist/optimistic.d.ts +26 -0
  66. package/dist/page.d.ts +2 -0
  67. package/dist/picker-WQN53YJ5.js +1 -0
  68. package/dist/pin-direction.d.ts +12 -0
  69. package/dist/pin-stacks.d.ts +3 -0
  70. package/dist/polling.d.ts +10 -0
  71. package/dist/postgres/001_initial.sql +128 -0
  72. package/dist/postgres/002_thread_tags.sql +1 -0
  73. package/dist/postgres/003_project_webhooks.sql +4 -0
  74. package/dist/project-management.d.ts +2 -0
  75. package/dist/react-inspector.d.ts +43 -0
  76. package/dist/react-props-panel-XSMDR2CY.js +1 -0
  77. package/dist/react-props-panel.d.ts +39 -0
  78. package/dist/react.d.ts +38 -0
  79. package/dist/react.js +1 -0
  80. package/dist/reaction-picker.d.ts +1 -0
  81. package/dist/review-layout.d.ts +23 -0
  82. package/dist/setup-browser.d.ts +1 -0
  83. package/dist/setup.d.ts +4 -0
  84. package/dist/setup.js +1 -0
  85. package/dist/shortcuts.d.ts +15 -0
  86. package/dist/static-svg.d.ts +3 -0
  87. package/dist/styles.d.ts +1 -0
  88. package/dist/tag-editor-VDPZ6CO3.js +1 -0
  89. package/dist/tag-editor.d.ts +12 -0
  90. package/dist/toolbar-runtime-2SIUE6SQ.js +1 -0
  91. package/dist/toolbar-runtime.d.ts +5 -0
  92. package/dist/types.d.ts +163 -0
  93. package/package.json +114 -4
  94. package/server/digest.ts +8 -0
  95. package/server/import-project.ts +164 -0
  96. package/server/index.ts +1656 -0
  97. package/server/migrations/0001_comments.sql +54 -0
  98. package/server/migrations/0002_identity.sql +2 -0
  99. package/server/migrations/0003_accent_color.sql +1 -0
  100. package/server/migrations/0004_workspaces.sql +46 -0
  101. package/server/migrations/0005_domains.sql +6 -0
  102. package/server/migrations/0006_member_budget.sql +16 -0
  103. package/server/migrations/0007_revisions.sql +17 -0
  104. package/server/migrations/0008_starter_comments.sql +1 -0
  105. package/server/migrations/0009_retention.sql +19 -0
  106. package/server/migrations/0010_reviewer_history.sql +1 -0
  107. package/server/migrations/0011_project_management.sql +52 -0
  108. package/server/migrations/0012_setup_sites.sql +3 -0
  109. package/server/migrations/0013_project_sites.sql +8 -0
  110. package/server/migrations/0014_project_site_removals.sql +3 -0
  111. package/server/migrations/0015_query_efficiency.sql +6 -0
  112. package/server/migrations/0016_thread_tags.sql +1 -0
  113. package/server/migrations/0017_project_webhooks.sql +4 -0
  114. package/server/node-webhooks.ts +126 -0
  115. package/server/project-management.ts +387 -0
  116. package/server/project-sites.ts +117 -0
  117. package/server/secrets.d.ts +24 -0
  118. package/server/setup-client.txt +2932 -0
  119. package/server/setup-page.ts +14 -0
  120. package/server/tsconfig.json +12 -0
  121. package/server/validation.ts +220 -0
  122. package/server/webhooks.ts +206 -0
  123. package/server/workspaces.ts +168 -0
  124. package/src/emoji.ts +7 -0
  125. package/src/page.ts +4 -0
  126. package/src/types.ts +166 -0
package/cli/agent.mjs ADDED
@@ -0,0 +1,615 @@
1
+ import { projectHelp, runProject } from "./project.mjs";
2
+ import {
3
+ readFile,
4
+ mkdir,
5
+ writeFile,
6
+ rename,
7
+ rm,
8
+ chmod,
9
+ stat,
10
+ } from "node:fs/promises";
11
+ import { resolve, dirname, join } from "node:path";
12
+ import { homedir } from "node:os";
13
+ import { createHash, randomBytes } from "node:crypto";
14
+ import { createServer } from "node:http";
15
+ import { spawn } from "node:child_process";
16
+ import { branchName } from "./config.mjs";
17
+ import { agentWorkflow, agentPrompt } from "../dist/agent-prompt.js";
18
+
19
+ import { installAgentWorkflow } from "./workflow.mjs";
20
+
21
+ const actions = {
22
+ list: "List threads; defaults to open. --status open|resolved|all, --page /path, --limit 50, --offset 0",
23
+ tag: "Add a group tag: tag THREAD_ID TAG",
24
+ untag: "Remove a group tag: untag THREAD_ID TAG",
25
+ get: "Read one complete thread: get THREAD_ID",
26
+ prompt: "Export open feedback as Markdown; --page /path, --json",
27
+ create: "Create a thread: --page /path --anchor-file FILE --body TEXT",
28
+ reply: "Reply to a thread: reply THREAD_ID --body TEXT",
29
+ resolve: "Resolve a thread: resolve THREAD_ID",
30
+ reopen: "Reopen a thread: reopen THREAD_ID",
31
+ edit: "Edit your message: edit THREAD_ID COMMENT_ID --body TEXT",
32
+ delete: "Delete your message: delete THREAD_ID COMMENT_ID",
33
+ react:
34
+ "Set your reaction: react THREAD_ID COMMENT_ID --emoji EMOJI [--remove]",
35
+ move: "Move a thread: move THREAD_ID --anchor-file FILE",
36
+ };
37
+ export const agentCommands = [
38
+ "login",
39
+ "logout",
40
+ "whoami",
41
+ "comments",
42
+ "schema",
43
+ "agents",
44
+ "project",
45
+ ];
46
+ export const agentHelp = `
47
+ Agent commands:
48
+ komo login Sign in with Google once for this project
49
+ komo whoami Check the current session
50
+ komo logout Revoke and remove the saved session
51
+ komo comments list Read open feedback as JSON
52
+ komo comments get ID Read a thread with all replies and context
53
+ komo comments prompt Export open feedback as an agent prompt
54
+ komo comments reply ID --body "Fixed; verified on mobile."
55
+ komo comments resolve ID Resolve after verification
56
+ komo comments reopen ID Reopen a thread
57
+ komo agents setup Install the default workflow in AGENTS.md
58
+ komo schema Machine-readable command reference
59
+
60
+ ${projectHelp}
61
+ Default workflow:
62
+ ${agentWorkflow}
63
+
64
+ ${Object.entries(actions)
65
+ .map(([name, description]) => ` ${name}: ${description}`)
66
+ .join("\n")}
67
+
68
+ Settings: .komo/project.json (searched upward), or --project KEY.
69
+ Overrides: --endpoint URL --origin SITE_URL --repo OWNER/REPO --branch NAME
70
+ Bodies: --body TEXT, --body-file FILE, or --body-file - for stdin.
71
+ Credentials: Google login, or KOMO_TOKEN for non-interactive use. Never commit tokens.
72
+ Results are JSON except prompt (Markdown unless --json). Errors are JSON on stderr.
73
+ `;
74
+
75
+ function parse(args) {
76
+ const booleans = new Set(["--json", "--remove", "--no-open"]);
77
+ const values = new Set([
78
+ "--out",
79
+ "--file",
80
+ "--access",
81
+ "--email",
82
+ "--user",
83
+ "--invite",
84
+ "--confirm",
85
+ "--project",
86
+ "--endpoint",
87
+ "--origin",
88
+ "--repo",
89
+ "--branch",
90
+ "--page",
91
+ "--status",
92
+ "--tag",
93
+ "--body",
94
+ "--body-file",
95
+ "--anchor-file",
96
+ "--emoji",
97
+ "--limit",
98
+ "--offset",
99
+ ]);
100
+ const flags = {},
101
+ positional = [];
102
+ for (let i = 0; i < args.length; i++) {
103
+ const arg = args[i];
104
+ if (booleans.has(arg)) flags[arg.slice(2)] = true;
105
+ else if (values.has(arg)) {
106
+ if (!args[i + 1] || args[i + 1].startsWith("--"))
107
+ throw Error(`Missing value for ${arg}.`);
108
+ flags[arg.slice(2)] = args[++i];
109
+ } else if (arg.startsWith("--")) throw Error(`Unknown option: ${arg}`);
110
+ else positional.push(arg);
111
+ }
112
+ return { flags, positional };
113
+ }
114
+ function safeUrl(value, originOnly = false) {
115
+ const url = new URL(value);
116
+ if (
117
+ url.username ||
118
+ url.password ||
119
+ url.search ||
120
+ url.hash ||
121
+ (url.protocol !== "https:" &&
122
+ !(
123
+ url.protocol === "http:" &&
124
+ ["localhost", "127.0.0.1"].includes(url.hostname)
125
+ ))
126
+ )
127
+ throw Error(
128
+ "Use HTTPS, or HTTP on localhost, without credentials, query, or fragment."
129
+ );
130
+ if (originOnly && url.pathname !== "/")
131
+ throw Error("Use a website origin without a path.");
132
+ return originOnly ? url.origin : url.href.replace(/\/$/, "");
133
+ }
134
+ async function configuration(flags, cwd, env) {
135
+ let directory = cwd,
136
+ settings = {};
137
+ while (true) {
138
+ try {
139
+ settings = JSON.parse(
140
+ await readFile(join(directory, ".komo/project.json"), "utf8")
141
+ );
142
+ break;
143
+ } catch (error) {
144
+ if (error.code !== "ENOENT") throw error;
145
+ }
146
+ if (dirname(directory) === directory) break;
147
+ directory = dirname(directory);
148
+ }
149
+ const project = flags.project || env.KOMO_PROJECT || settings.project;
150
+ if (!project)
151
+ throw Error("Run komo init in this repository, or pass --project KEY.");
152
+ const endpoint = safeUrl(
153
+ flags.endpoint ||
154
+ env.KOMO_ENDPOINT ||
155
+ settings.endpoint ||
156
+ "https://komo.offbr.co"
157
+ );
158
+ const origin = safeUrl(
159
+ flags.origin ||
160
+ env.KOMO_ORIGIN ||
161
+ settings.origin ||
162
+ "http://localhost:3000",
163
+ true
164
+ );
165
+ const branch =
166
+ flags.branch ||
167
+ env.KOMO_BRANCH ||
168
+ (settings.scope === "branch" ? branchName(env, directory) : "shared");
169
+ if (!branch) throw Error("Cannot detect branch. Pass --branch NAME.");
170
+ return {
171
+ project,
172
+ endpoint,
173
+ origin,
174
+ branch,
175
+ repo: flags.repo || env.KOMO_REPO || settings.repo || project,
176
+ };
177
+ }
178
+ function credentialPath(config, env) {
179
+ const key = createHash("sha256")
180
+ .update(`${config.endpoint}\n${config.project}`)
181
+ .digest("hex");
182
+ return join(
183
+ env.KOMO_CONFIG_HOME || join(homedir(), ".config", "komo"),
184
+ `${key}.json`
185
+ );
186
+ }
187
+ async function readToken(path, env) {
188
+ if (env.KOMO_TOKEN) return env.KOMO_TOKEN;
189
+ try {
190
+ return JSON.parse(await readFile(path, "utf8")).token;
191
+ } catch (error) {
192
+ if (error.code !== "ENOENT") throw error;
193
+ return null;
194
+ }
195
+ }
196
+ async function saveToken(path, token) {
197
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
198
+ await chmod(dirname(path), 0o700);
199
+ const temporary = `${path}.${randomBytes(8).toString("hex")}.tmp`;
200
+ try {
201
+ await writeFile(temporary, JSON.stringify({ token }), {
202
+ mode: 0o600,
203
+ flag: "wx",
204
+ });
205
+ await rename(temporary, path);
206
+ } finally {
207
+ await rm(temporary, { force: true });
208
+ }
209
+ }
210
+ export function apiClient(config, token) {
211
+ return async (path, method = "GET", data) => {
212
+ const url = new URL(path, `${config.endpoint}/`);
213
+ for (const key of ["project", "repo", "branch"])
214
+ url.searchParams.set(key, config[key]);
215
+ const response = await fetch(url, {
216
+ method,
217
+ redirect: "error",
218
+ signal: AbortSignal.timeout(15000),
219
+ headers: {
220
+ Origin: config.origin,
221
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
222
+ ...(data === undefined ? {} : { "Content-Type": "application/json" }),
223
+ },
224
+ body: data === undefined ? undefined : JSON.stringify(data),
225
+ });
226
+ let result;
227
+ try {
228
+ result = await response.json();
229
+ } catch {
230
+ throw Error(`API returned an invalid response (${response.status}).`);
231
+ }
232
+ if (!response.ok) {
233
+ const error = Error(
234
+ response.status === 401
235
+ ? "Session missing or expired. Run komo login."
236
+ : result.error || `Request failed (${response.status}).`
237
+ );
238
+ error.status = response.status;
239
+ throw error;
240
+ }
241
+ return result;
242
+ };
243
+ }
244
+ export async function listThreads(request) {
245
+ const threads = [],
246
+ seen = new Set();
247
+ let offset = 0;
248
+ while (offset !== null) {
249
+ if (
250
+ seen.has(offset) ||
251
+ !Number.isInteger(offset) ||
252
+ offset < 0 ||
253
+ offset > 100000
254
+ )
255
+ throw Error("Invalid pagination from API.");
256
+ seen.add(offset);
257
+ const result = await request(`threads?offset=${offset}`);
258
+ if (!Array.isArray(result.threads))
259
+ throw Error("Invalid thread list from API.");
260
+ threads.push(...result.threads);
261
+ offset = result.next ?? null;
262
+ }
263
+ return threads;
264
+ }
265
+ async function smallFile(path, maximum) {
266
+ if ((await stat(path)).size > maximum)
267
+ throw Error("Input file is too large.");
268
+ return readFile(path, "utf8");
269
+ }
270
+ async function inputBody(flags, cwd) {
271
+ if (flags.body !== undefined && flags["body-file"] !== undefined)
272
+ throw Error("Choose --body or --body-file, not both.");
273
+ let text = flags.body;
274
+ if (flags["body-file"] === "-") {
275
+ const chunks = [];
276
+ let bytes = 0;
277
+ for await (const chunk of process.stdin) {
278
+ bytes += chunk.length;
279
+ if (bytes > 16000) throw Error("Body is too large.");
280
+ chunks.push(chunk);
281
+ }
282
+ text = Buffer.concat(chunks).toString("utf8");
283
+ } else if (flags["body-file"])
284
+ text = await smallFile(resolve(cwd, flags["body-file"]), 16000);
285
+ if (!text?.trim() || text.length > 4000)
286
+ throw Error("Supply a nonempty body of at most 4,000 characters.");
287
+ return text.trim();
288
+ }
289
+ async function anchorFile(flags, cwd) {
290
+ if (!flags["anchor-file"])
291
+ throw Error("Supply --anchor-file FILE with the captured anchor JSON.");
292
+ return JSON.parse(await smallFile(resolve(cwd, flags["anchor-file"]), 16000));
293
+ }
294
+ function id(value, label = "thread") {
295
+ if (!value || !/^[\w:-]{1,100}$/.test(value))
296
+ throw Error(`Supply a valid ${label} ID.`);
297
+ return value;
298
+ }
299
+
300
+ async function login(config, path, flags) {
301
+ const nonce = randomBytes(32).toString("hex");
302
+ let complete, rejectLogin, authUrl;
303
+ const done = new Promise((resolveLogin, reject) => {
304
+ complete = resolveLogin;
305
+ rejectLogin = reject;
306
+ });
307
+ void done.catch(() => {});
308
+ const server = createServer(async (req, res) => {
309
+ const send = (status, text, type = "text/plain") => {
310
+ res.writeHead(status, {
311
+ "Content-Type": `${type}; charset=utf-8`,
312
+ "Cache-Control": "no-store",
313
+ "Content-Security-Policy": `default-src 'none'; script-src 'nonce-${nonce}'; connect-src 'self'; style-src 'unsafe-inline'; frame-ancestors 'none'; base-uri 'none'`,
314
+ "Referrer-Policy": "no-referrer",
315
+ });
316
+ res.end(text);
317
+ };
318
+ if (req.headers.host !== new URL(loopback).host || req.url !== `/${nonce}`)
319
+ return send(404, "Not found");
320
+ if (req.method === "GET") {
321
+ const safe = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
322
+ return send(
323
+ 200,
324
+ `<!doctype html><meta charset="utf-8"><title>Sign in to komo</title><style>body{background:#191919;color:#eee;font:16px system-ui;max-width:420px;margin:18vh auto;padding:24px}button{font:inherit;padding:14px 20px;border:0;border-radius:12px;background:#c8b5f4}p{line-height:1.5;color:#aaa}</style><h1>Connect your agent</h1><p id="project"></p><p>The CLI can read and update comments as you. Your session stays on this computer.</p><button>Continue with Google</button><p id="status"></p><script nonce="${nonce}">document.querySelector('#project').textContent=${safe(config.project)};let popup;document.querySelector('button').onclick=()=>{popup=window.open(${safe(authUrl)},'komo-cli-auth','width=500,height=700')};window.addEventListener('message',async e=>{if(e.origin!==${safe(new URL(config.endpoint).origin)}||e.source!==popup||e.data?.type!=='branch-comments:auth')return;const response=await fetch(location.pathname,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:e.data.token})});document.querySelector('#status').textContent=await response.text();if(response.ok){document.querySelector('button').disabled=true;setTimeout(()=>window.close(),400)}});</script>`,
325
+ "text/html"
326
+ );
327
+ }
328
+ if (
329
+ req.method !== "POST" ||
330
+ req.headers.origin !== loopback ||
331
+ req.headers["content-type"] !== "application/json"
332
+ )
333
+ return send(403, "Not allowed");
334
+ try {
335
+ const chunks = [];
336
+ let size = 0;
337
+ for await (const chunk of req) {
338
+ size += chunk.length;
339
+ if (size > 4096) return send(413, "Too large");
340
+ chunks.push(chunk);
341
+ }
342
+ const { token } = JSON.parse(Buffer.concat(chunks).toString("utf8"));
343
+ if (typeof token !== "string" || token.length > 200 || !token.length)
344
+ throw Error("Invalid session.");
345
+ const { user } = await apiClient(config, token)("me");
346
+ if (!user.verified) throw Error("Use Google sign-in to connect the CLI.");
347
+ await saveToken(path, token);
348
+ res.once("finish", () => complete(user));
349
+ send(200, "Connected. You can close this tab.");
350
+ } catch {
351
+ send(400, "Sign-in failed. Please try again.");
352
+ }
353
+ });
354
+ server.requestTimeout = 15000;
355
+ server.headersTimeout = 10000;
356
+ await new Promise((ready, reject) => {
357
+ server.once("error", reject);
358
+ server.listen(0, "127.0.0.1", ready);
359
+ });
360
+ const loopback = `http://127.0.0.1:${server.address().port}`;
361
+ const timeout = setTimeout(
362
+ () => rejectLogin(Error("Sign-in timed out. Run komo login again.")),
363
+ 600000
364
+ );
365
+ const cancel = () => rejectLogin(Error("Sign-in cancelled."));
366
+ process.once("SIGINT", cancel);
367
+ try {
368
+ authUrl = (
369
+ await apiClient(config)("auth/google/start", "POST", {
370
+ returnOrigin: loopback,
371
+ })
372
+ ).url;
373
+ if (new URL(authUrl).origin !== new URL(config.endpoint).origin)
374
+ throw Error("Unexpected sign-in URL.");
375
+ const url = `${loopback}/${nonce}`;
376
+ console.error(`Complete Google sign-in in your browser:\n${url}`);
377
+ if (!flags["no-open"]) {
378
+ const opener =
379
+ process.platform === "darwin"
380
+ ? "open"
381
+ : process.platform === "win32"
382
+ ? "rundll32"
383
+ : "xdg-open";
384
+ const argv =
385
+ process.platform === "win32"
386
+ ? ["url.dll,FileProtocolHandler", url]
387
+ : [url];
388
+ const child = spawn(opener, argv, { stdio: "ignore", detached: true });
389
+ child.on("error", () => {});
390
+ child.unref();
391
+ }
392
+ return await done;
393
+ } finally {
394
+ clearTimeout(timeout);
395
+ process.removeListener("SIGINT", cancel);
396
+ server.close();
397
+ server.closeAllConnections();
398
+ }
399
+ }
400
+
401
+ export async function runAgent(
402
+ args,
403
+ { cwd = process.cwd(), env = process.env } = {}
404
+ ) {
405
+ const { flags, positional } = parse(args);
406
+ const [command, action = "list", threadId, commentId] = positional;
407
+ if (command === "agents") {
408
+ if (
409
+ action !== "setup" ||
410
+ positional.length !== 2 ||
411
+ Object.keys(flags).length
412
+ )
413
+ throw Error("Use komo agents setup from the project root.");
414
+ console.log(
415
+ JSON.stringify(
416
+ { ok: true, data: await installAgentWorkflow(cwd) },
417
+ null,
418
+ 2
419
+ )
420
+ );
421
+ return;
422
+ }
423
+ if (command === "schema") {
424
+ console.log(
425
+ JSON.stringify(
426
+ {
427
+ version: 1,
428
+ commands: actions,
429
+ auth: "komo login",
430
+ output: "JSON; prompt is Markdown unless --json",
431
+ context: ".komo/project.json or --project KEY",
432
+ overrides: ["--endpoint", "--origin", "--repo", "--branch"],
433
+ body: ["--body", "--body-file", "--body-file -"],
434
+ setup: "komo agents setup",
435
+ project: projectHelp,
436
+ guidance: agentWorkflow,
437
+ },
438
+ null,
439
+ 2
440
+ )
441
+ );
442
+ return;
443
+ }
444
+ if (command === "comments" && !Object.hasOwn(actions, action))
445
+ throw Error(`Unknown comments action: ${action}`);
446
+ const expected =
447
+ command === "project"
448
+ ? 2
449
+ : command === "comments"
450
+ ? ["edit", "delete", "react", "tag", "untag"].includes(action)
451
+ ? 4
452
+ : ["get", "reply", "resolve", "reopen", "move"].includes(action)
453
+ ? 3
454
+ : 2
455
+ : 1;
456
+ if (positional.length > expected)
457
+ throw Error("Unexpected positional arguments. Run komo --help.");
458
+ const config = await configuration(flags, cwd, env),
459
+ path = credentialPath(config, env);
460
+ const token = await readToken(path, env),
461
+ request = apiClient(config, token);
462
+ if (command !== "logout" && config.repo === config.project) {
463
+ const metadata = await request("config");
464
+ if (metadata.repo) config.repo = metadata.repo;
465
+ }
466
+ let data;
467
+ if (command === "login") data = { user: await login(config, path, flags) };
468
+ else {
469
+ if (!token)
470
+ throw Error("No saved session. Run komo login, or set KOMO_TOKEN.");
471
+ if (command === "project")
472
+ data = await runProject(action, flags, request, config, cwd);
473
+ else if (command === "whoami") data = await request("me");
474
+ else if (command === "logout") {
475
+ try {
476
+ await request("me", "DELETE");
477
+ } catch (error) {
478
+ if (error.status !== 401 && error.status !== 404) throw error;
479
+ }
480
+ await rm(path, { force: true });
481
+ data = { signedOut: true };
482
+ } else if (["list", "get", "prompt"].includes(action)) {
483
+ let threads = (
484
+ action === "get"
485
+ ? (await request(`threads?id=${id(threadId)}`)).threads
486
+ : await listThreads(request)
487
+ ).map((thread) => ({
488
+ ...thread,
489
+ comments: thread.comments
490
+ .filter((comment) => comment.body !== "[Comment deleted]")
491
+ .map((comment) => ({
492
+ ...comment,
493
+ author: {
494
+ id: comment.author.id,
495
+ name: comment.author.name,
496
+ verified: comment.author.verified,
497
+ },
498
+ })),
499
+ resolvedBy: thread.resolvedBy
500
+ ? {
501
+ id: thread.resolvedBy.id,
502
+ name: thread.resolvedBy.name,
503
+ verified: thread.resolvedBy.verified,
504
+ }
505
+ : null,
506
+ }));
507
+ if (action === "get") {
508
+ data = threads.find((thread) => thread.id === id(threadId));
509
+ if (!data) throw Error("Thread not found in this project and branch.");
510
+ } else {
511
+ if (flags.page)
512
+ threads = threads.filter(
513
+ (thread) =>
514
+ thread.page === flags.page.replace(/\/$/, "") ||
515
+ thread.page === flags.page
516
+ );
517
+ if (action === "prompt") {
518
+ const prompt =
519
+ agentPrompt(threads, {
520
+ ...config,
521
+ ...(flags.page
522
+ ? {
523
+ page:
524
+ flags.page === "/" ? "/" : flags.page.replace(/\/$/, ""),
525
+ }
526
+ : {}),
527
+ }) || "No open comments.";
528
+ if (!flags.json) {
529
+ console.log(prompt);
530
+ return;
531
+ }
532
+ data = { prompt };
533
+ } else {
534
+ const status = flags.status || "open";
535
+ if (!["open", "resolved", "all"].includes(status))
536
+ throw Error("Use --status open, resolved, or all.");
537
+ const filtered = threads.filter(
538
+ (thread) =>
539
+ status === "all" || thread.resolved === (status === "resolved")
540
+ ).filter((thread) => !flags.tag || (thread.tags ?? []).includes(String(flags.tag).toLowerCase()));
541
+ const limit = Number(flags.limit ?? 50),
542
+ offset = Number(flags.offset ?? 0);
543
+ if (
544
+ !Number.isInteger(limit) ||
545
+ limit < 1 ||
546
+ limit > 250 ||
547
+ !Number.isInteger(offset) ||
548
+ offset < 0
549
+ )
550
+ throw Error("Use --limit 1–250 and a nonnegative --offset.");
551
+ data = {
552
+ threads: filtered.slice(offset, offset + limit),
553
+ total: filtered.length,
554
+ nextOffset:
555
+ offset + limit < filtered.length ? offset + limit : null,
556
+ };
557
+ }
558
+ }
559
+ } else if (action === "create") {
560
+ if (!flags.page?.startsWith("/")) throw Error("Supply --page /path.");
561
+ data = await request("threads", "POST", {
562
+ page: flags.page,
563
+ anchor: await anchorFile(flags, cwd),
564
+ body: await inputBody(flags, cwd),
565
+ });
566
+ } else {
567
+ const thread = `threads/${id(threadId)}`;
568
+ if (action === "tag" || action === "untag") {
569
+ if (!commentId) throw Error("Supply a tag name.");
570
+ data = await request(thread, "PATCH", { [action]: commentId });
571
+ } else if (["resolve", "reopen"].includes(action))
572
+ data = await request(thread, "PATCH", {
573
+ resolved: action === "resolve",
574
+ });
575
+ else if (action === "reply")
576
+ data = await request(`${thread}/comments`, "POST", {
577
+ body: await inputBody(flags, cwd),
578
+ });
579
+ else if (action === "move")
580
+ data = await request(thread, "PATCH", {
581
+ anchor: { ...(await anchorFile(flags, cwd)), unstacked: true },
582
+ });
583
+ else {
584
+ const comment = `${thread}/comments/${id(commentId, "comment")}`;
585
+ if (action === "edit")
586
+ data = await request(comment, "PATCH", {
587
+ body: await inputBody(flags, cwd),
588
+ });
589
+ else if (action === "delete") data = await request(comment, "DELETE");
590
+ else if (action === "react") {
591
+ if (!flags.emoji) throw Error("Supply --emoji EMOJI.");
592
+ data = await request(`${comment}/reactions`, "POST", {
593
+ emoji: flags.emoji,
594
+ active: !flags.remove,
595
+ });
596
+ }
597
+ }
598
+ }
599
+ }
600
+ console.log(
601
+ JSON.stringify(
602
+ {
603
+ ok: true,
604
+ scope: {
605
+ project: config.project,
606
+ repo: config.repo,
607
+ branch: config.branch,
608
+ },
609
+ data,
610
+ },
611
+ null,
612
+ 2
613
+ )
614
+ );
615
+ }
package/cli/config.mjs ADDED
@@ -0,0 +1,34 @@
1
+ import { execFileSync } from "node:child_process";
2
+
3
+ export function gitValue(args, cwd) {
4
+ try {
5
+ return execFileSync("git", args, {
6
+ cwd,
7
+ encoding: "utf8",
8
+ stdio: ["ignore", "pipe", "ignore"],
9
+ }).trim();
10
+ } catch {
11
+ return "";
12
+ }
13
+ }
14
+ export function repository(remote) {
15
+ const match = remote.match(
16
+ /^(?:https?:\/\/[^/]+\/|ssh:\/\/(?:[^@/]+@)?[^/]+\/|(?:[^@/]+@)?[^:/]+:)(.+?)(?:\.git)?\/?$/
17
+ );
18
+ return match?.[1] || "";
19
+ }
20
+ export function branchName(env, cwd) {
21
+ return (
22
+ env.KOMO_BRANCH ||
23
+ env.CF_PAGES_BRANCH ||
24
+ env.WORKERS_CI_BRANCH ||
25
+ env.VERCEL_GIT_COMMIT_REF ||
26
+ env.GITHUB_HEAD_REF ||
27
+ env.GITHUB_REF_NAME ||
28
+ env.BRANCH ||
29
+ gitValue(["symbolic-ref", "--short", "HEAD"], cwd)
30
+ );
31
+ }
32
+ export function clientModule(config) {
33
+ return `import { defineKomo } from '@hazem3500/komo/setup';\n\nexport const initKomo = defineKomo(${JSON.stringify(config, null, 2)});\n`;
34
+ }