@hazem3500/komo 0.9.2 → 0.9.5

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 (44) hide show
  1. package/NODE.md +1 -1
  2. package/README.md +84 -9
  3. package/cli/agent.mjs +122 -27
  4. package/cli/design-edit.mjs +269 -0
  5. package/dist/adapters/css-variables.d.ts +9 -0
  6. package/dist/adapters/css-variables.js +1 -0
  7. package/dist/adapters/react-fiber.d.ts +27 -0
  8. package/dist/adapters/react-fiber.js +1 -0
  9. package/dist/agent-prompt.d.ts +3 -1
  10. package/dist/agent-prompt.js +1 -1
  11. package/dist/{chunk-Y2GRLW4Q.js → chunk-DH22G2S5.js} +1 -1
  12. package/dist/chunk-NWRSH6AL.js +1 -0
  13. package/dist/design-comment-MPAF2YAN.js +1 -0
  14. package/dist/design-export-DD2NJI4O.js +1 -0
  15. package/dist/design-export.d.ts +9 -2
  16. package/dist/design-mode-styles.d.ts +1 -1
  17. package/dist/design-mode.js +1 -1
  18. package/dist/design-panel-NU2WUI4D.js +1 -0
  19. package/dist/design-panel-styles.d.ts +1 -1
  20. package/dist/design-system.d.ts +24 -5
  21. package/dist/index.js +1 -1
  22. package/dist/{inline-text-editor-3JLDAUPO.js → inline-text-editor-JWVS7J75.js} +1 -1
  23. package/dist/node.mjs +513 -18
  24. package/dist/postgres/004_personal_access_tokens.sql +25 -0
  25. package/dist/react-props-panel-BW4ZQPD7.js +1 -0
  26. package/dist/react-props-panel.d.ts +19 -2
  27. package/dist/react.d.ts +1 -1
  28. package/dist/react.js +1 -1
  29. package/dist/select-menu-V7RYOZFK.js +1 -0
  30. package/dist/select-menu.d.ts +5 -0
  31. package/dist/setup.js +1 -1
  32. package/package.json +14 -3
  33. package/server/account-page.ts +231 -0
  34. package/server/index.ts +62 -5
  35. package/server/migrations/0018_personal_access_tokens.sql +25 -0
  36. package/server/secrets.d.ts +2 -0
  37. package/server/setup-client.txt +54 -59
  38. package/server/tokens.ts +324 -0
  39. package/server/workspaces.ts +2 -0
  40. package/dist/chunk-HCGLCO3R.js +0 -1
  41. package/dist/design-comment-MWNQNKWT.js +0 -1
  42. package/dist/design-export-ZHCBPSWY.js +0 -1
  43. package/dist/design-panel-BPVXFERR.js +0 -1
  44. package/dist/react-props-panel-R5XEVATL.js +0 -1
package/NODE.md CHANGED
@@ -63,7 +63,7 @@ initKomo({
63
63
  });
64
64
  ```
65
65
 
66
- Point the existing CLI at the same endpoint, project, repository, and branch. `komo login` uses Google when configured; `KOMO_TOKEN` accepts a previously issued session token for a non-interactive bot. Store that token as a secret. It has the same permissions as its user. For example:
66
+ Point the existing CLI at the same endpoint, project, repository, and branch. `komo login` uses Google when configured; `KOMO_TOKEN` accepts a personal access token (create one at `/account?project=KEY` on your server; see Bots and CI in the README) or a previously issued session token for a non-interactive bot. Store that token as a secret. Access tokens are limited by their scopes and projects; session tokens have the same permissions as their user. Optionally set `KOMO_TOKEN_PEPPER` to a long random secret to mix into stored token hashes; changing it revokes every access token. For example:
67
67
 
68
68
  ```sh
69
69
  komo comments list --endpoint https://comments.example.com --project YOUR_GENERATED_PROJECT_KEY --repo your-org/your-repo --branch pr-42 --origin https://pr-42.preview.example.com
package/README.md CHANGED
@@ -97,7 +97,7 @@ Design mode also takes interaction cues from established design tools: select a
97
97
 
98
98
  React teams can register selected component roots for typed prop previews through the `./react` entry. A build-time TypeScript Compiler API pass generates the prop manifest from an explicitly marked function component and props type; the inspector edits only strings, numbers, booleans, and literal unions. Functions, `ReactNode`, objects, generics, nullable unions, and any unsupported type remain read-only. This metadata is a safe allowlist, not runtime discovery of every React component; `defaultProps` and every React component form are not inferred.
99
99
 
100
- The app provides an `onChange` callback that updates its own state. Komo previews those values through React's normal render cycle, offers undo/reset, and can prefill an explicit comment with the proposed before/after values and source path. Posting still requires the user to submit the comment. Komo never accesses React Fiber internals or writes source files. Preview prop changes reset on refresh. A component may opt one leaf render into direct text editing by setting `textProp` to the name of an allowed string prop; Komo does not infer which child text belongs to a prop. See [`examples/react-props`](./examples/react-props/README.md) for the generated fixture and limitations.
100
+ The app provides an `onChange` callback that updates its own state. Komo previews those values through React's normal render cycle, offers undo/reset, and can prefill an explicit comment with the proposed before/after values and source path. Posting still requires the user to submit the comment. Komo's core never accesses React Fiber internals and never writes source files; the one exception is the separate, opt-in [React fiber adapter](#discovering-react-components), which reads React internals only for apps that install it. Preview prop changes reset on refresh. A component may opt one leaf render into direct text editing by setting `textProp` to the name of an allowed string prop; Komo does not infer which child text belongs to a prop. See [`examples/react-props`](./examples/react-props/README.md) for the generated fixture and limitations.
101
101
 
102
102
  ### Opt-in design tokens
103
103
 
@@ -107,7 +107,7 @@ Compatible registered tokens appear inside each existing style control, next to
107
107
 
108
108
  Shared theme edits remain secondary under **Edit shared theme values**. Their explicit Preview action changes the registered variable temporarily and reports the author-declared impact selector/count. This is not a persisted design-system edit; it resets on refresh, widget destruction, or Reset. Selectors are estimates, not proof that every matching element consumes the variable. Both element and shared changes remain previews until the user explicitly submits a comment; Komo does not write CSS or React source. Enable registration only in a trusted development/demo environment, and exclude confidential values from the token list.
109
109
 
110
- This approach uses React Developer Tools as a reference for component props but avoids depending on its browser DevTools UI. TypeScript's compiler can generate type metadata before type erasure, while a host callback keeps live previews within the app's public state model. React 19 is exercised by the fixture; the adapter uses public hooks and DOM refs and does not inspect version-specific React internals.
110
+ This approach uses React Developer Tools as a reference for component props but avoids depending on its browser DevTools UI. TypeScript's compiler can generate type metadata before type erasure, while a host callback keeps live previews within the app's public state model. React 18 and 19 are both exercised (see the fixture); the registration hook uses public hooks and DOM refs and does not inspect version-specific React internals. Only the opt-in React fiber adapter does.
111
111
 
112
112
  Puck is a useful reference for the **explicit component contract**: its config registers renderable components and declares typed editor fields, defaults, nested object/array fields, and global/component/per-instance edit permissions. Komo borrows the opt-in registration, typed allowlist, contextual controls, and fail-closed edit permissions. It does not reuse Puck's editor/runtime: Puck owns a structured page document and supports component insertion, nesting, drag-and-drop, and publishing, while Komo edits a temporary preview of the host app's existing React state and turns proposed values into a comment. A Puck dependency would add a full page-builder interaction/data model and runtime cost where Komo needs a small optional inspector; Puck's MIT license permits reuse, but licensing is not the deciding constraint. For the first version, nested object/array, slot, custom, and dynamic fields remain read-only until the adapter can represent and validate them safely without implying broader source persistence.
113
113
 
@@ -117,15 +117,26 @@ Sources: [Figma layer properties and styles](https://help.figma.com/hc/en-us/art
117
117
 
118
118
  ### Design-system adapters
119
119
 
120
- `design: { tokens, themes, components }` tells Design mode about your design system. These are types in `DesignSystem`, and all three are optional. komo never guesses tokens from computed styles or reads framework internals; your adapters answer.
120
+ `design: { tokens, themes, components }` tells Design mode about your design system. These are types in `DesignSystem`, and all three are optional. komo never guesses tokens from computed styles, and its core never reads framework internals; your adapters answer. The single exception is the opt-in [React fiber adapter](#discovering-react-components): it is off unless you install it, and it is the only komo code that touches React's private fields.
121
121
 
122
122
  - `tokens: TokenAdapter` lists tokens (aliases resolved, per theme). For a picked element, it says which token each CSS property uses, or that the value is off-token. It can also preview a token, for example by swapping a utility class, and describe how to apply the change in source.
123
- - `themes: ThemeAxis[]` are independent axes reviewers can flip, such as light/dark or a product palette. Proposals record the theme state they were made in.
124
- - `components: ComponentAdapter` identifies the component behind an element (name, props, source), so Design mode shows "Button (variant=secondary)". It works alongside `useKomoInspectable`.
123
+ - `themes: ThemeAxis[]` are independent axes reviewers can flip from the Design panel, such as light/dark or a product palette. Proposals record the theme state they were made in.
124
+ - `components: ComponentAdapter` identifies the component behind an element (name, props, source). Proposals name it with its props. A component registered with `useKomoInspectable` always takes precedence and keeps its editable props. An adapter may also supply `panel(identity)` to show read-only details in the Design panel.
125
125
 
126
- Design proposals also carry a versioned `DesignEdit` record (token swaps, prop changes, text, and a raw CSS fallback) that the CLI and agents can apply.
126
+ Each Design mode comment starts with its context, for example `Theme: mode=dark`, `Component: Button (src/ui/Button.tsx:12)` and `Props: variant="secondary"`. Its changes follow: token swaps, shared token values, prop changes, text, and raw CSS as a fallback. The CLI reads the comment back as a versioned `DesignEdit` record (`theme`, `target.component`, `changes`) that agents can apply; comments without context lines still parse. See [Agent CLI](#agent-cli).
127
127
 
128
- The API is landing in stages, and this section is updated as each part ships. Token adapters work today; themes, components and the `DesignEdit` record follow.
128
+ #### Themes
129
+
130
+ Each axis gets a row at the top of the Design panel: a segmented control for two or three options, a select for more. Switching calls `apply(id)`, then the panel re-reads `current()`, lists tokens for the new theme state, and refreshes the picked element's values, chips and outline.
131
+
132
+ ```ts
133
+ import { classTheme } from "@hazem3500/komo/adapters/class-theme";
134
+
135
+ const mode = classTheme({ id: "mode", options: [{ id: "light" }, { id: "dark", class: "dark" }], colorScheme: true });
136
+ initKomo({ project: "your-project", design: { themes: [mode] } });
137
+ ```
138
+
139
+ Theme is context, not an edit. Switching isn't counted as a change, and Undo, Reset and Before/After leave it alone. When Design mode closes, or the widget is destroyed, komo applies each axis's original option again. An axis whose `current()` throws is left out; an `apply()` that throws doesn't break the panel. Any object with `id`, `options`, `current()` and `apply()` works, so a theme kept in React state or a cookie fits too.
129
140
 
130
141
  #### Token adapters
131
142
 
@@ -175,6 +186,40 @@ initKomo({ project: "your-project", design: { tokens: classTokens } });
175
186
 
176
187
  Keep `resolve()` cheap and honest: report only properties you can vouch for. Define the adapter once, outside render, so `useKomo` sees the same object. The React props demo shows this adapter with `?tokens=classes`.
177
188
 
189
+ #### Discovering CSS variables
190
+
191
+ `discoverCssTokens()` from `@hazem3500/komo/adapters/css-variables` is the no-table option for sites whose tokens are CSS custom properties, including the variables Tailwind v4 emits for `@theme`. It is opt-in, reads `--*` declarations on `:root`/`html` from same-origin stylesheets (cross-origin sheets are skipped), and is meant for development builds.
192
+
193
+ ```ts
194
+ import { discoverCssTokens } from "@hazem3500/komo/adapters/css-variables";
195
+
196
+ initKomo({ project, design: { tokens: discoverCssTokens() } });
197
+ ```
198
+
199
+ - Tailwind namespaces set the type and the properties a token may style: `--color-*`, `--spacing`/`--spacing-*`, `--radius-*`, `--text-*`, `--font-*`, `--font-weight-*`, `--leading-*`. Other variables are kept only when their value is a color or a length. `--tw-*` is skipped. Paths follow the name, so `--color-brand-500` is `color.brand-500`.
200
+ - A property shows a token only when the element's winning declaration (inline, then stylesheet by `!important`, specificity and order) is exactly one `var()` of a discovered variable. An equal value is not a match.
201
+ - Choosing a token previews `property: var(--name)`, and proposals say `Replace var(--old) with var(--new) for property`, so agents write the reference, not the resolved value.
202
+ - `include(name)` filters variables and `limit` caps the list (300). Values come from the computed style, so they follow the current theme. The scan reruns on every `list()`; `@layer` and `@media` blocks are read, but variables set only under a theme selector such as `[data-mode=dark]` are not listed on their own.
203
+ - Selector-based impact counts and **Edit shared theme values** need `useKomoDesignTokens` and are not part of this adapter.
204
+
205
+ #### Discovering React components
206
+
207
+ `discoverReactComponents()` from `@hazem3500/komo/adapters/react-fiber` finds the React component behind a picked element, so Design mode can show its name, props and where it was rendered without registering each component by hand. It is **opt-in and off by default**, and it is the one place komo reads React's private fields. Use it in development builds.
208
+
209
+ ```ts
210
+ import { discoverReactComponents } from "@hazem3500/komo/adapters/react-fiber";
211
+
212
+ initKomo({ project, design: { components: discoverReactComponents() } });
213
+ // Keep your own adapter first: discoverReactComponents({ adapter: myAdapter })
214
+ ```
215
+
216
+ - **What it reads.** The `__reactFiber$*` key React puts on DOM nodes (or, when present, the renderers of `window.__REACT_DEVTOOLS_GLOBAL_HOOK__`), then `return`/`child` links, `memoizedProps`, `_debugSource` (React 18) and `_debugStack` (React 19 dev). It climbs from the picked element, through shadow hosts, to the nearest function, class, `forwardRef` or `memo` component, and picks that fiber's current copy so props are not stale after updates. Several React roots and renderers on a page are fine.
217
+ - **What it shows.** The panel adds a **Discovered props** group labelled "discovered from React, read-only", with the props and "rendered at `file:line`". The location is where the component was written into JSX (the call site), not where it is defined. React 19 records an Error stack of the generated file; a dev server that serves modules unbundled gives real paths, and otherwise pass `resolveSource(file, line, column)` to map positions (for example through a source map). Proposals also name the component with its primitive props, like any component adapter.
218
+ - **Props are summarized, never run.** Strings, numbers and booleans are shown; arrays and plain objects are cut off by `maxDepth` (3), `maxItems` (6), `maxString` (80) and `maxValue` (240), and `maxProps` caps the list (24). Functions, React elements, symbols, DOM nodes and class instances are described (`ƒ onSave()`, `<button />`), cycles read `[Circular]`, and getters are never invoked. Props named like secrets (`token`, `password`, `apiKey`, `secret`, `auth`…) and anything `redact(name)` matches show as `[redacted]` and are kept out of proposals.
219
+ - **Read-only, and registered components win.** Discovered props are never editable, because komo has no state callback or typed schema for them. A component registered with `useKomoInspectable`, or answered by your own `adapter`, always takes precedence and keeps its editable typed props; around an element inside a registered component the panel keeps showing "Inside *Component*". A component nested inside a registered one that is not registered itself is discovered.
220
+ - **Graceful no-op.** Every access is guarded. Non-React pages, unknown React versions or fiber shapes, production builds and hostile props yield nothing rather than an error. Production React keeps no debug fields, so there is no location; names that look minified (one or two characters) or are missing are not shown. `include(name)` hides components you don't want listed.
221
+ - **Fragile by design.** These fields are private React implementation details, and React can rename or remove them in any release. React 18.3 and 19.2 are tested here (unit tests and a Chrome run of the demo for each); another version may quietly show nothing until the adapter is updated. If that matters more than convenience, register the components you care about with `useKomoInspectable` instead.
222
+
178
223
  #### Tailwind and class themes
179
224
 
180
225
  Two optional adapters ship as their own entry points, outside the core bundle. For utility-class design systems, a token is a class: komo previews by swapping classes, not by setting CSS variables.
@@ -258,14 +303,44 @@ For an existing inline installation, pass `--project YOUR_PROJECT_KEY --origin h
258
303
 
259
304
  `--body-file FILE` reads a message from disk; `--body-file -` reads stdin. Use one body source. Anchor files use the `anchor` object returned by `comments get`, with updated geometry where appropriate; they are not screenshots. The API validates selectors, geometry, permissions, and quotas.
260
305
 
261
- Successful commands emit `{ "ok": true, "scope": { ... }, "data": ... }`. Errors emit `{ "ok": false, "error": { "message": "...", "status": ... } }` on stderr and exit with code 1. Prompts are Markdown unless `--json` is present. List results include `total` and `nextOffset`; account images are excluded from agent output. Commands do not automatically retry writes.
306
+ Successful commands emit `{ "ok": true, "scope": { ... }, "data": ... }`. Errors emit `{ "ok": false, "error": { "message": "...", "status": ... } }` on stderr and exit with code 1. Prompts are Markdown unless `--json` is present. Design mode messages include `designEdit`, a validated `DesignEdit` (`komo: "design-edit"`, `version: 1`) with the target selector, component, token adapter and changes. `komo comments prompt` prints it after the message as a fenced `json komo-design-edit` block with a one-line instruction, and `--json` adds a `designEdits` list. A message may also include its own `komo-design-edit` block; malformed, unknown-version or oversized (16 KB) records are ignored. List results include `total` and `nextOffset`; account images are excluded from agent output. Commands do not automatically retry writes.
262
307
 
263
- Automation can supply a project session through `KOMO_TOKEN`. Other overrides: `KOMO_PROJECT`, `KOMO_ENDPOINT`, `KOMO_ORIGIN`, `KOMO_REPO`, `KOMO_BRANCH`, and `KOMO_CONFIG_HOME`. Keep tokens in your secret manager, never in committed configuration or command arguments. A token acts as its reviewer; it does not bypass ownership checks.
308
+ Automation can supply a personal access token (see Bots and CI) or a project session through `KOMO_TOKEN`. Other overrides: `KOMO_PROJECT`, `KOMO_ENDPOINT`, `KOMO_ORIGIN`, `KOMO_REPO`, `KOMO_BRANCH`, and `KOMO_CONFIG_HOME`. Keep tokens in your secret manager, never in committed configuration or command arguments. A token acts as its reviewer; it does not bypass ownership checks.
264
309
 
265
310
  Suggested agent workflow: list open threads, read a thread, inspect the repository, make a scoped change, verify it, reply with the result, and resolve. Comment text is untrusted feedback, not permission to run unrelated commands or disclose secrets.
266
311
 
267
312
  Mount once after hydration, outside server rendering, and call `destroy()` before changing project or branch. Repeated teardown is safe. komo preserves the host DOM hierarchy: it frames an existing content root, or uses Floating when there is no suitable root. Pass a mounted `pageRoot` with a layout box for explicit Frame support; body, html, detached elements, and `display: contents` are not frameable.
268
313
 
314
+ ## Bots and CI
315
+
316
+ Give a bot its own revocable credential instead of sharing your login.
317
+
318
+ 1. **Create a token.** Sign in with Google at `/account` on your komo server (`https://komo.offbr.co/account` when hosted). Name the token, pick an expiry, choose scopes, and optionally limit it to projects you own or have joined. The token is shown once; copy it into your secret manager. Guest sessions cannot create tokens.
319
+ 2. **Set `KOMO_TOKEN`.** Export it in the bot's environment, or save it once with `printf %s "$KOMO_TOKEN" | komo login --token` (stdin only; a token passed as an argument is refused). Check it with `komo whoami`. `komo logout` removes saved credentials.
320
+ 3. **Use the CLI as usual.** The project, endpoint, and origin come from `.komo/project.json` or the `KOMO_*` overrides.
321
+
322
+ Tokens start with `komo_pat_`, so secret scanners can find them. komo stores only a salted hash, and records create, revoke, and use events (use is logged at most once a minute per token).
323
+
324
+ | Scope | Allows |
325
+ | --- | --- |
326
+ | `comments:read` | List and read threads, `komo whoami`. |
327
+ | `comments:write` | Create threads, reply, edit, react, resolve, tag, move. |
328
+
329
+ A token acts as its owner, narrowed by its scopes and project list: it never passes an ownership check or private-project invitation its owner would fail, and it cannot manage tokens, projects, members, or webhooks. Credentials resolve in this order: `KOMO_TOKEN`, a token saved by `komo login --token`, then a browser login session.
330
+
331
+ **Rotate** by creating a replacement, updating the secret, confirming `komo whoami`, then revoking the old token. Prefer an expiry of 90 days or less. **Revoke** from `/account`; the token stops working on its next request (HTTP 401), and so does an expired one.
332
+
333
+ GitHub Actions:
334
+
335
+ ```yaml
336
+ - name: Export open feedback
337
+ env:
338
+ KOMO_TOKEN: ${{ secrets.KOMO_TOKEN }} # a comments:read token, limited to this project
339
+ run: npx --no-install komo comments prompt
340
+ ```
341
+
342
+ Add the token under **Settings → Secrets and variables → Actions**. Self-hosted servers can set `KOMO_TOKEN_PEPPER` (a long random secret) to mix into stored hashes; changing it revokes every token.
343
+
269
344
  ## How it works
270
345
 
271
346
  The package adds an isolated ShadowRoot to your site. Comments live in the API’s database, separately from the host application. Reviewers using the same project and scope see the same feedback. Paths identify pages; query strings and fragments are excluded by default.
package/cli/agent.mjs CHANGED
@@ -15,6 +15,7 @@ import { createServer } from "node:http";
15
15
  import { spawn } from "node:child_process";
16
16
  import { branchName } from "./config.mjs";
17
17
  import { agentWorkflow, agentPrompt } from "../dist/agent-prompt.js";
18
+ import { designEditFromComment, formatDesignEdit } from "./design-edit.mjs";
18
19
 
19
20
  import { installAgentWorkflow } from "./workflow.mjs";
20
21
 
@@ -46,8 +47,9 @@ export const agentCommands = [
46
47
  export const agentHelp = `
47
48
  Agent commands:
48
49
  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
50
+ komo login --token Save an access token read from stdin (bots and CI)
51
+ komo whoami Check the current credentials
52
+ komo logout Revoke the saved session and remove saved credentials
51
53
  komo comments list Read open feedback as JSON
52
54
  komo comments get ID Read a thread with all replies and context
53
55
  komo comments prompt Export open feedback as an agent prompt
@@ -68,12 +70,14 @@ ${Object.entries(actions)
68
70
  Settings: .komo/project.json (searched upward), or --project KEY.
69
71
  Overrides: --endpoint URL --origin SITE_URL --repo OWNER/REPO --branch NAME
70
72
  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.
73
+ Credentials: KOMO_TOKEN, then a saved access token, then a Google login session. Never commit tokens.
74
+ Create access tokens at /account on your komo server; pass one with: printf %s "$TOKEN" | komo login --token
72
75
  Results are JSON except prompt (Markdown unless --json). Errors are JSON on stderr.
76
+ Design mode messages also carry designEdit: a parsed komo DesignEdit v1 record.
73
77
  `;
74
78
 
75
79
  function parse(args) {
76
- const booleans = new Set(["--json", "--remove", "--no-open"]);
80
+ const booleans = new Set(["--json", "--remove", "--no-open", "--token"]);
77
81
  const values = new Set([
78
82
  "--out",
79
83
  "--file",
@@ -184,15 +188,51 @@ function credentialPath(config, env) {
184
188
  `${key}.json`
185
189
  );
186
190
  }
187
- async function readToken(path, env) {
188
- if (env.KOMO_TOKEN) return env.KOMO_TOKEN;
191
+ const PERSONAL_TOKEN = /^komo_pat_[a-z0-9]{12}_[A-Za-z0-9_-]{43}$/;
192
+ // A personal access token works for every project on one server, so it is
193
+ // stored per endpoint, beside the per-project login sessions.
194
+ function personalTokenPath(config, env) {
195
+ const key = createHash("sha256").update(config.endpoint).digest("hex");
196
+ return join(
197
+ env.KOMO_CONFIG_HOME || join(homedir(), ".config", "komo"),
198
+ `pat-${key}.json`
199
+ );
200
+ }
201
+ async function readSaved(path) {
189
202
  try {
190
- return JSON.parse(await readFile(path, "utf8")).token;
203
+ return JSON.parse(await readFile(path, "utf8")).token || null;
191
204
  } catch (error) {
192
205
  if (error.code !== "ENOENT") throw error;
193
206
  return null;
194
207
  }
195
208
  }
209
+ // Precedence: KOMO_TOKEN, then a saved access token, then a login session.
210
+ async function readCredentials(sessionPath, patPath, env) {
211
+ const saved = { session: await readSaved(sessionPath), pat: await readSaved(patPath) };
212
+ const [source, token] = env.KOMO_TOKEN
213
+ ? ["env", env.KOMO_TOKEN]
214
+ : saved.pat
215
+ ? ["token", saved.pat]
216
+ : saved.session
217
+ ? ["session", saved.session]
218
+ : [null, null];
219
+ return { source, token, saved };
220
+ }
221
+ async function readTokenFromStdin() {
222
+ if (process.stdin.isTTY)
223
+ throw Error("Pipe the token on stdin, for example: printf %s \"$TOKEN\" | komo login --token");
224
+ const chunks = [];
225
+ let bytes = 0;
226
+ for await (const chunk of process.stdin) {
227
+ bytes += chunk.length;
228
+ if (bytes > 512) throw Error("That does not look like a komo access token.");
229
+ chunks.push(chunk);
230
+ }
231
+ const token = Buffer.concat(chunks).toString("utf8").trim();
232
+ if (!PERSONAL_TOKEN.test(token))
233
+ throw Error("Expected a komo access token (komo_pat_…) on stdin.");
234
+ return token;
235
+ }
196
236
  async function saveToken(path, token) {
197
237
  await mkdir(dirname(path), { recursive: true, mode: 0o700 });
198
238
  await chmod(dirname(path), 0o700);
@@ -232,7 +272,9 @@ export function apiClient(config, token) {
232
272
  if (!response.ok) {
233
273
  const error = Error(
234
274
  response.status === 401
235
- ? "Session missing or expired. Run komo login."
275
+ ? token?.startsWith("komo_pat_")
276
+ ? "Access token invalid, expired, or revoked. Create a new one at /account."
277
+ : "Session missing or expired. Run komo login."
236
278
  : result.error || `Request failed (${response.status}).`
237
279
  );
238
280
  error.status = response.status;
@@ -402,8 +444,16 @@ export async function runAgent(
402
444
  args,
403
445
  { cwd = process.cwd(), env = process.env } = {}
404
446
  ) {
447
+ if (args.some((arg) => arg.includes("komo_pat_") || arg.startsWith("--token=")))
448
+ throw Error(
449
+ "Never pass a token as an argument: it would appear in shell history and process lists. Pipe it on stdin: printf %s \"$TOKEN\" | komo login --token"
450
+ );
405
451
  const { flags, positional } = parse(args);
406
452
  const [command, action = "list", threadId, commentId] = positional;
453
+ if (flags.token && (command !== "login" || positional.length > 1))
454
+ throw Error(
455
+ "Use --token only as: printf %s \"$TOKEN\" | komo login --token. The token itself goes on stdin, never in an argument."
456
+ );
407
457
  if (command === "agents") {
408
458
  if (
409
459
  action !== "setup" ||
@@ -426,8 +476,10 @@ export async function runAgent(
426
476
  {
427
477
  version: 1,
428
478
  commands: actions,
429
- auth: "komo login",
479
+ auth: "KOMO_TOKEN, komo login --token (stdin), or komo login",
430
480
  output: "JSON; prompt is Markdown unless --json",
481
+ designEdit:
482
+ "Design mode messages carry designEdit (komo DesignEdit v1); prompt prints it as a json komo-design-edit block",
431
483
  context: ".komo/project.json or --project KEY",
432
484
  overrides: ["--endpoint", "--origin", "--repo", "--branch"],
433
485
  body: ["--body", "--body-file", "--body-file -"],
@@ -457,27 +509,46 @@ export async function runAgent(
457
509
  throw Error("Unexpected positional arguments. Run komo --help.");
458
510
  const config = await configuration(flags, cwd, env),
459
511
  path = credentialPath(config, env);
460
- const token = await readToken(path, env),
512
+ const patPath = personalTokenPath(config, env);
513
+ const { source, token, saved } = await readCredentials(path, patPath, env),
461
514
  request = apiClient(config, token);
462
515
  if (command !== "logout" && config.repo === config.project) {
463
516
  const metadata = await request("config");
464
517
  if (metadata.repo) config.repo = metadata.repo;
465
518
  }
466
519
  let data;
467
- if (command === "login") data = { user: await login(config, path, flags) };
520
+ if (command === "login" && flags.token) {
521
+ const given = await readTokenFromStdin();
522
+ // Verify against the API before saving, so a typo never replaces a working token.
523
+ const { user } = await apiClient(config, given)("me");
524
+ await saveToken(patPath, given);
525
+ data = { user, auth: "token" };
526
+ } else if (command === "login")
527
+ data = { user: await login(config, path, flags) };
468
528
  else {
469
529
  if (!token)
470
- throw Error("No saved session. Run komo login, or set KOMO_TOKEN.");
530
+ throw Error(
531
+ "Not signed in. Run komo login, pipe a token to komo login --token, or set KOMO_TOKEN."
532
+ );
471
533
  if (command === "project")
472
534
  data = await runProject(action, flags, request, config, cwd);
473
- else if (command === "whoami") data = await request("me");
535
+ else if (command === "whoami") data = { ...(await request("me")), auth: source };
474
536
  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
- }
537
+ // Only login sessions are revoked here. Access tokens are revoked on
538
+ // the account page, so logout never destroys a token other jobs share.
539
+ const session = env.KOMO_TOKEN && !env.KOMO_TOKEN.startsWith("komo_pat_")
540
+ ? env.KOMO_TOKEN
541
+ : saved.session;
542
+ if (session)
543
+ try {
544
+ await apiClient(config, session)("me", "DELETE");
545
+ } catch (error) {
546
+ if (error.status !== 401 && error.status !== 404) throw error;
547
+ }
480
548
  await rm(path, { force: true });
549
+ await rm(patPath, { force: true });
550
+ if (env.KOMO_TOKEN)
551
+ console.error("KOMO_TOKEN is still set in this environment; unset it to stop using it.");
481
552
  data = { signedOut: true };
482
553
  } else if (["list", "get", "prompt"].includes(action)) {
483
554
  let threads = (
@@ -488,14 +559,21 @@ export async function runAgent(
488
559
  ...thread,
489
560
  comments: thread.comments
490
561
  .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
- })),
562
+ .map((comment) => {
563
+ const designEdit = designEditFromComment(
564
+ comment.body,
565
+ thread.anchor?.selector
566
+ );
567
+ return {
568
+ ...comment,
569
+ author: {
570
+ id: comment.author.id,
571
+ name: comment.author.name,
572
+ verified: comment.author.verified,
573
+ },
574
+ ...(designEdit ? { designEdit } : {}),
575
+ };
576
+ }),
499
577
  resolvedBy: thread.resolvedBy
500
578
  ? {
501
579
  id: thread.resolvedBy.id,
@@ -524,12 +602,29 @@ export async function runAgent(
524
602
  flags.page === "/" ? "/" : flags.page.replace(/\/$/, ""),
525
603
  }
526
604
  : {}),
605
+ annotate: (comment) =>
606
+ comment.designEdit && formatDesignEdit(comment.designEdit),
527
607
  }) || "No open comments.";
528
608
  if (!flags.json) {
529
609
  console.log(prompt);
530
610
  return;
531
611
  }
532
- data = { prompt };
612
+ const page =
613
+ flags.page && (flags.page === "/" ? "/" : flags.page.replace(/\/$/, ""));
614
+ data = {
615
+ prompt,
616
+ designEdits: threads
617
+ .filter((thread) => !thread.resolved && (!page || thread.page === page))
618
+ .flatMap((thread) =>
619
+ thread.comments
620
+ .filter((comment) => comment.designEdit)
621
+ .map((comment) => ({
622
+ thread: thread.id,
623
+ comment: comment.id,
624
+ designEdit: comment.designEdit,
625
+ }))
626
+ ),
627
+ };
533
628
  } else {
534
629
  const status = flags.status || "open";
535
630
  if (!["open", "resolved", "all"].includes(status))