@hazem3500/palette 0.9.108 → 0.9.110

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 (115) hide show
  1. package/NODE.md +32 -30
  2. package/README.md +58 -46
  3. package/cli/agent.mjs +93 -69
  4. package/cli/config.mjs +17 -1
  5. package/cli/design-edit.mjs +9 -7
  6. package/cli/index.mjs +26 -24
  7. package/cli/snapshots.mjs +3 -3
  8. package/cli/workflow.mjs +15 -7
  9. package/dist/{accent-picker-TTIZ65XF.js → accent-picker-PPVPURA7.js} +1 -1
  10. package/dist/account-tokens-LBRVZAEJ.js +1 -0
  11. package/dist/adapters/css-variables.js +1 -1
  12. package/dist/adapters/tailwind.js +1 -1
  13. package/dist/chunk-2FX2JEMO.js +1 -0
  14. package/dist/chunk-4HBBEVFK.js +1 -0
  15. package/dist/chunk-5HZI7HRR.js +1 -0
  16. package/dist/chunk-DU7H4TEX.js +1 -0
  17. package/dist/chunk-FGZXQUAD.js +1 -0
  18. package/dist/{chunk-AYGLN3MI.js → chunk-HLXSRXTK.js} +1 -1
  19. package/dist/{chunk-DZRM3YUQ.js → chunk-J47R52WX.js} +1 -1
  20. package/dist/chunk-KELFDOEC.js +1 -0
  21. package/dist/{chunk-2J3IID4N.js → chunk-KLPQTFVH.js} +1 -1
  22. package/dist/chunk-ONUORDYE.js +1 -0
  23. package/dist/chunk-PF4NYFZK.js +1 -0
  24. package/dist/chunk-S6NEYT36.js +1 -0
  25. package/dist/chunk-U55ASP2Y.js +1 -0
  26. package/dist/chunk-VX6XCU4E.js +1 -0
  27. package/dist/color-editor-QPJQD36F.js +1 -0
  28. package/dist/config.d.ts +1 -1
  29. package/dist/design-comment-BF2KGPWU.js +1 -0
  30. package/dist/{design-export-BNNHI4JP.js → design-export-Y2GU3KER.js} +1 -1
  31. package/dist/design-geometry-HJLEXLPO.js +1 -0
  32. package/dist/design-mode-styles.d.ts +1 -1
  33. package/dist/design-mode.js +1 -1
  34. package/dist/design-panel-ZQS4WQCI.js +1 -0
  35. package/dist/design-panel-styles.d.ts +1 -1
  36. package/dist/design-popup.d.ts +1 -1
  37. package/dist/design-system.d.ts +5 -4
  38. package/dist/{field-controls-4ROTPXMO.js → field-controls-4AIIXPNQ.js} +1 -1
  39. package/dist/{frame-iframe-WBEP3R6S.js → frame-iframe-PYLYMCOK.js} +1 -1
  40. package/dist/frame-size-TF6SIUFO.js +1 -0
  41. package/dist/frame-target.d.ts +5 -2
  42. package/dist/host-motion.d.ts +1 -1
  43. package/dist/index.js +1 -1
  44. package/dist/inline-text-editor-MK6FHDAM.js +1 -0
  45. package/dist/live-colors-D426RBEC.js +1 -0
  46. package/dist/live-colors.d.ts +1 -1
  47. package/dist/node.mjs +102 -75
  48. package/dist/onboarding-6OSADOWE.js +1 -0
  49. package/dist/{pin-groups-YAYFPJYA.js → pin-groups-GF53RTHP.js} +1 -1
  50. package/dist/{pin-move-PG54WWPU.js → pin-move-G6EWHLDO.js} +1 -1
  51. package/dist/public-env.d.ts +49 -11
  52. package/dist/react-props-panel-ZSKQ44GS.js +1 -0
  53. package/dist/react.d.ts +2 -0
  54. package/dist/react.js +1 -1
  55. package/dist/select-menu-OZVNHZ73.js +1 -0
  56. package/dist/server.js +80 -57
  57. package/dist/setup.js +1 -1
  58. package/dist/shell-mode-PLV6F474.js +1 -0
  59. package/dist/shell-session.d.ts +5 -3
  60. package/dist/shell.d.ts +1 -1
  61. package/dist/shell.js +1 -1
  62. package/dist/shortcut-help-DIMOT2ZK.js +1 -0
  63. package/dist/snapshot-3PSQKXUV.js +1 -0
  64. package/dist/snapshot.d.ts +1 -1
  65. package/dist/source-info.d.ts +1 -1
  66. package/dist/storage-keys.d.ts +5 -0
  67. package/dist/tag-editor-B3I7WD4N.js +1 -0
  68. package/dist/thread-snapshot-ML2RZVID.js +1 -0
  69. package/dist/token-custom-CASAQWYW.js +1 -0
  70. package/dist/token-page-NIB5HAVS.js +1 -0
  71. package/dist/token-picker-AJ5E5IN5.js +1 -0
  72. package/dist/{toolbar-runtime-QCZONSWE.js → toolbar-runtime-TZWY2KH3.js} +1 -1
  73. package/dist/typed-custom.d.ts +1 -1
  74. package/dist/types.d.ts +1 -1
  75. package/package.json +1 -1
  76. package/server/account-page.ts +6 -6
  77. package/server/iap.ts +2 -2
  78. package/server/index.ts +30 -24
  79. package/server/project-management.ts +5 -5
  80. package/server/proxy-auth.ts +17 -16
  81. package/server/public-env.ts +67 -11
  82. package/server/setup-client.txt +173 -173
  83. package/server/setup-page.ts +1 -1
  84. package/server/tokens.ts +9 -6
  85. package/server/webhooks.ts +9 -5
  86. package/src/types.ts +1 -1
  87. package/dist/account-tokens-PINQNSNE.js +0 -1
  88. package/dist/chunk-4KEVPMJJ.js +0 -1
  89. package/dist/chunk-4XQD75A2.js +0 -1
  90. package/dist/chunk-57MG3M5P.js +0 -1
  91. package/dist/chunk-5OVDW2RU.js +0 -1
  92. package/dist/chunk-6HM4JQLV.js +0 -1
  93. package/dist/chunk-DLC3CRFV.js +0 -1
  94. package/dist/chunk-DVYSFT6O.js +0 -1
  95. package/dist/chunk-FD4GACV2.js +0 -1
  96. package/dist/chunk-IQOPM3JF.js +0 -1
  97. package/dist/chunk-UEZCLURX.js +0 -1
  98. package/dist/color-editor-ATWWSENS.js +0 -1
  99. package/dist/design-comment-RXUCDTYA.js +0 -1
  100. package/dist/design-geometry-DPMHGQEK.js +0 -1
  101. package/dist/design-panel-HTN6LR3V.js +0 -1
  102. package/dist/frame-size-MRCN6XYD.js +0 -1
  103. package/dist/inline-text-editor-AMFRKW5Z.js +0 -1
  104. package/dist/live-colors-GWOVVQSO.js +0 -1
  105. package/dist/onboarding-BVTCFEHL.js +0 -1
  106. package/dist/react-props-panel-4XO3NSX3.js +0 -1
  107. package/dist/select-menu-QAYV7NKJ.js +0 -1
  108. package/dist/shell-mode-SUYLWDWW.js +0 -1
  109. package/dist/shortcut-help-XRDNX7MZ.js +0 -1
  110. package/dist/snapshot-V264ZMQG.js +0 -1
  111. package/dist/tag-editor-MMAMOEI4.js +0 -1
  112. package/dist/thread-snapshot-HLTNHS4N.js +0 -1
  113. package/dist/token-custom-UCA6CBOV.js +0 -1
  114. package/dist/token-page-PGPWPUKU.js +0 -1
  115. package/dist/token-picker-WMWUKLKJ.js +0 -1
package/cli/agent.mjs CHANGED
@@ -14,7 +14,7 @@ import { homedir } from "node:os";
14
14
  import { createHash, randomBytes } from "node:crypto";
15
15
  import { createServer } from "node:http";
16
16
  import { spawn } from "node:child_process";
17
- import { branchName } from "./config.mjs";
17
+ import { branchName, setting } from "./config.mjs";
18
18
  import { agentWorkflow, agentPrompt } from "../dist/agent-prompt.js";
19
19
  import { designEditFromComment, formatDesignEdit } from "./design-edit.mjs";
20
20
 
@@ -26,7 +26,7 @@ const actions = {
26
26
  untag: "Remove a group tag: untag THREAD_ID TAG",
27
27
  get: "Read one complete thread: get THREAD_ID; --snapshots also saves its element snapshots as files",
28
28
  prompt: "Export open feedback as Markdown; --page /path, --json, --snapshots to save element snapshots and list their files",
29
- snapshots: "Save a thread's element snapshots as files: snapshots THREAD_ID [--out DIR] (default .komo/snapshots)",
29
+ snapshots: "Save a thread's element snapshots as files: snapshots THREAD_ID [--out DIR] (default .palette/snapshots, or .komo/snapshots in an older project)",
30
30
  create: "Create a thread: --page /path --anchor-file FILE --body TEXT",
31
31
  reply: "Reply to a thread: reply THREAD_ID --body TEXT",
32
32
  resolve: "Resolve a thread: resolve THREAD_ID",
@@ -70,10 +70,10 @@ ${Object.entries(actions)
70
70
  .map(([name, description]) => ` ${name}: ${description}`)
71
71
  .join("\n")}
72
72
 
73
- Settings: .komo/project.json (searched upward), or --project KEY.
73
+ Settings: .palette/project.json, or .komo/project.json from before the rename (searched upward), or --project KEY.
74
74
  Overrides: --endpoint URL --origin SITE_URL --repo OWNER/REPO --branch NAME
75
75
  Bodies: --body TEXT, --body-file FILE, or --body-file - for stdin.
76
- Credentials: KOMO_TOKEN, then a saved access token, then a Google login session. Never commit tokens.
76
+ Credentials: PALETTE_TOKEN (or the older KOMO_TOKEN), then a saved access token, then a Google login session. Never commit tokens.
77
77
  Create access tokens at /account on your Palette server; pass one with: printf %s "$TOKEN" | palette login --token
78
78
  Results are JSON except prompt (Markdown unless --json). Errors are JSON on stderr.
79
79
  Design mode messages also carry designEdit: a parsed Palette DesignEdit v1 record.
@@ -143,39 +143,44 @@ function safeUrl(value, originOnly = false) {
143
143
  async function configuration(flags, cwd, env) {
144
144
  let directory = cwd,
145
145
  settings = {},
146
- root = cwd;
147
- while (true) {
148
- try {
149
- settings = JSON.parse(
150
- await readFile(join(directory, ".komo/project.json"), "utf8")
151
- );
152
- root = directory;
153
- break;
154
- } catch (error) {
155
- if (error.code !== "ENOENT") throw error;
156
- }
146
+ root = cwd,
147
+ settingsDir = ".palette";
148
+ search: while (true) {
149
+ // .komo/ is where a project set up before the rename keeps its settings.
150
+ for (const dir of [".palette", ".komo"])
151
+ try {
152
+ settings = JSON.parse(
153
+ await readFile(join(directory, dir, "project.json"), "utf8")
154
+ );
155
+ root = directory;
156
+ settingsDir = dir;
157
+ break search;
158
+ } catch (error) {
159
+ if (error.code !== "ENOENT") throw error;
160
+ }
157
161
  if (dirname(directory) === directory) break;
158
162
  directory = dirname(directory);
159
163
  }
160
- const project = flags.project || env.KOMO_PROJECT || settings.project;
164
+ const project = flags.project || setting(env, "PROJECT") || settings.project;
161
165
  if (!project)
162
166
  throw Error("Run palette init in this repository, or pass --project KEY.");
163
- const endpoint = safeUrl(
164
- flags.endpoint ||
165
- env.KOMO_ENDPOINT ||
166
- settings.endpoint ||
167
- "https://komo.offbr.co"
168
- );
167
+ const configured =
168
+ flags.endpoint || setting(env, "ENDPOINT") || settings.endpoint;
169
+ if (!configured)
170
+ throw Error(
171
+ "No Palette server configured. Pass --endpoint URL, set PALETTE_ENDPOINT, or add endpoint to .palette/project.json."
172
+ );
173
+ const endpoint = safeUrl(configured);
169
174
  const origin = safeUrl(
170
175
  flags.origin ||
171
- env.KOMO_ORIGIN ||
176
+ setting(env, "ORIGIN") ||
172
177
  settings.origin ||
173
178
  "http://localhost:3000",
174
179
  true
175
180
  );
176
181
  const branch =
177
182
  flags.branch ||
178
- env.KOMO_BRANCH ||
183
+ setting(env, "BRANCH") ||
179
184
  (settings.scope === "branch" ? branchName(env, directory) : "shared");
180
185
  if (!branch) throw Error("Cannot detect branch. Pass --branch NAME.");
181
186
  return {
@@ -183,42 +188,55 @@ async function configuration(flags, cwd, env) {
183
188
  endpoint,
184
189
  origin,
185
190
  branch,
186
- repo: flags.repo || env.KOMO_REPO || settings.repo || project,
191
+ repo: flags.repo || setting(env, "REPO") || settings.repo || project,
187
192
  root,
193
+ settingsDir,
188
194
  };
189
195
  }
190
- function credentialPath(config, env) {
196
+ // Credentials are saved under ~/.config/palette (or PALETTE_CONFIG_HOME). Ones
197
+ // saved before the rename, under ~/.config/komo, are still read; logout removes both.
198
+ function credentialFiles(env, name) {
199
+ const home = setting(env, "CONFIG_HOME");
200
+ return (home ? [home] : [join(homedir(), ".config", "palette"), join(homedir(), ".config", "komo")])
201
+ .map((directory) => join(directory, name));
202
+ }
203
+ function credentialPaths(config, env) {
191
204
  const key = createHash("sha256")
192
205
  .update(`${config.endpoint}\n${config.project}`)
193
206
  .digest("hex");
194
- return join(
195
- env.KOMO_CONFIG_HOME || join(homedir(), ".config", "komo"),
196
- `${key}.json`
197
- );
207
+ return credentialFiles(env, `${key}.json`);
198
208
  }
199
- const PERSONAL_TOKEN = /^komo_pat_[a-z0-9]{12}_[A-Za-z0-9_-]{43}$/;
209
+ const PERSONAL_TOKEN = /^(?:palette|komo)_pat_[a-z0-9]{12}_[A-Za-z0-9_-]{43}$/;
210
+ const isPersonalToken = (token) => /^(?:palette|komo)_pat_/.test(token ?? "");
200
211
  // A personal access token works for every project on one server, so it is
201
212
  // stored per endpoint, beside the per-project login sessions.
202
- function personalTokenPath(config, env) {
213
+ function personalTokenPaths(config, env) {
203
214
  const key = createHash("sha256").update(config.endpoint).digest("hex");
204
- return join(
205
- env.KOMO_CONFIG_HOME || join(homedir(), ".config", "komo"),
206
- `pat-${key}.json`
207
- );
215
+ return credentialFiles(env, `pat-${key}.json`);
208
216
  }
209
- async function readSaved(path) {
210
- try {
211
- return JSON.parse(await readFile(path, "utf8")).token || null;
212
- } catch (error) {
213
- if (error.code !== "ENOENT") throw error;
214
- return null;
217
+ async function readSaved(paths) {
218
+ for (const path of paths)
219
+ try {
220
+ return JSON.parse(await readFile(path, "utf8")).token || null;
221
+ } catch (error) {
222
+ if (error.code !== "ENOENT") throw error;
223
+ }
224
+ return null;
225
+ }
226
+ async function readEach(paths) {
227
+ const tokens = [];
228
+ for (const path of paths) {
229
+ const token = await readSaved([path]);
230
+ if (token) tokens.push(token);
215
231
  }
232
+ return tokens;
216
233
  }
217
- // Precedence: KOMO_TOKEN, then a saved access token, then a login session.
218
- async function readCredentials(sessionPath, patPath, env) {
219
- const saved = { session: await readSaved(sessionPath), pat: await readSaved(patPath) };
220
- const [source, token] = env.KOMO_TOKEN
221
- ? ["env", env.KOMO_TOKEN]
234
+ // Precedence: PALETTE_TOKEN (or KOMO_TOKEN), then a saved access token, then a login session.
235
+ async function readCredentials(sessionPaths, patPaths, env) {
236
+ const saved = { session: await readSaved(sessionPaths), pat: await readSaved(patPaths) };
237
+ const variable = setting(env, "TOKEN");
238
+ const [source, token] = variable
239
+ ? ["env", variable]
222
240
  : saved.pat
223
241
  ? ["token", saved.pat]
224
242
  : saved.session
@@ -238,7 +256,7 @@ async function readTokenFromStdin() {
238
256
  }
239
257
  const token = Buffer.concat(chunks).toString("utf8").trim();
240
258
  if (!PERSONAL_TOKEN.test(token))
241
- throw Error("Expected a Palette access token (komo_pat_…) on stdin.");
259
+ throw Error("Expected a Palette access token (palette_pat_…) on stdin.");
242
260
  return token;
243
261
  }
244
262
  async function saveToken(path, token) {
@@ -280,7 +298,7 @@ export function apiClient(config, token) {
280
298
  if (!response.ok) {
281
299
  const error = Error(
282
300
  response.status === 401
283
- ? token?.startsWith("komo_pat_")
301
+ ? isPersonalToken(token)
284
302
  ? "Access token invalid, expired, or revoked. Create a new one at /account."
285
303
  : "Session missing or expired. Run palette login."
286
304
  : result.error || `Request failed (${response.status}).`
@@ -371,7 +389,7 @@ async function login(config, path, flags) {
371
389
  const safe = (value) => JSON.stringify(value).replace(/</g, "\\u003c");
372
390
  return send(
373
391
  200,
374
- `<!doctype html><meta charset="utf-8"><title>Sign in to Palette</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>`,
392
+ `<!doctype html><meta charset="utf-8"><title>Sign in to Palette</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)},'palette-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>`,
375
393
  "text/html"
376
394
  );
377
395
  }
@@ -452,7 +470,7 @@ export async function runAgent(
452
470
  args,
453
471
  { cwd = process.cwd(), env = process.env } = {}
454
472
  ) {
455
- if (args.some((arg) => arg.includes("komo_pat_") || arg.startsWith("--token=")))
473
+ if (args.some((arg) => /(?:palette|komo)_pat_/.test(arg) || arg.startsWith("--token=")))
456
474
  throw Error(
457
475
  "Never pass a token as an argument: it would appear in shell history and process lists. Pipe it on stdin: printf %s \"$TOKEN\" | palette login --token"
458
476
  );
@@ -484,13 +502,13 @@ export async function runAgent(
484
502
  {
485
503
  version: 1,
486
504
  commands: actions,
487
- auth: "KOMO_TOKEN, palette login --token (stdin), or palette login",
505
+ auth: "PALETTE_TOKEN (or KOMO_TOKEN), palette login --token (stdin), or palette login",
488
506
  output: "JSON; prompt is Markdown unless --json",
489
507
  designEdit:
490
- "Design mode messages carry designEdit (Palette DesignEdit v1); prompt prints it as a json komo-design-edit block",
508
+ "Design mode messages carry designEdit (Palette DesignEdit v1); prompt prints it as a json palette-design-edit block",
491
509
  snapshots:
492
- "Threads may carry snapshot { image?, html? } URLs. palette comments snapshots ID (or get/prompt --snapshots) saves them under --out DIR, default .komo/snapshots, and reports the file paths",
493
- context: ".komo/project.json or --project KEY",
510
+ "Threads may carry snapshot { image?, html? } URLs. palette comments snapshots ID (or get/prompt --snapshots) saves them under --out DIR, default .palette/snapshots (.komo/snapshots in an older project), and reports the file paths",
511
+ context: ".palette/project.json (or .komo/project.json) or --project KEY",
494
512
  overrides: ["--endpoint", "--origin", "--repo", "--branch"],
495
513
  body: ["--body", "--body-file", "--body-file -"],
496
514
  setup: "palette agents setup",
@@ -518,9 +536,11 @@ export async function runAgent(
518
536
  if (positional.length > expected)
519
537
  throw Error("Unexpected positional arguments. Run palette --help.");
520
538
  const config = await configuration(flags, cwd, env),
521
- path = credentialPath(config, env);
522
- const patPath = personalTokenPath(config, env);
523
- const { source, token, saved } = await readCredentials(path, patPath, env),
539
+ paths = credentialPaths(config, env),
540
+ path = paths[0];
541
+ const patPaths = personalTokenPaths(config, env),
542
+ patPath = patPaths[0];
543
+ const { source, token, saved } = await readCredentials(paths, patPaths, env),
524
544
  request = apiClient(config, token);
525
545
  if (command !== "logout" && config.repo === config.project) {
526
546
  const metadata = await request("config");
@@ -528,8 +548,8 @@ export async function runAgent(
528
548
  }
529
549
  if (flags.snapshots && !(command === "comments" && ["get", "prompt"].includes(action)))
530
550
  throw Error("Use --snapshots with palette comments get or prompt.");
531
- // Downloaded snapshots go under --out, or .komo/snapshots beside the project settings.
532
- const snapshotDirectory = () => (flags.out ? resolve(cwd, flags.out) : join(config.root ?? cwd, ".komo/snapshots"));
551
+ // Downloaded snapshots go under --out, or snapshots/ in the project settings directory.
552
+ const snapshotDirectory = () => (flags.out ? resolve(cwd, flags.out) : join(config.root ?? cwd, config.settingsDir, "snapshots"));
533
553
  const snapshotDownload = (thread) => saveSnapshots(config, token, thread, snapshotDirectory(), !flags.out);
534
554
  let data;
535
555
  if (command === "login" && flags.token) {
@@ -543,7 +563,7 @@ export async function runAgent(
543
563
  else {
544
564
  if (!token)
545
565
  throw Error(
546
- "Not signed in. Run palette login, pipe a token to palette login --token, or set KOMO_TOKEN."
566
+ "Not signed in. Run palette login, pipe a token to palette login --token, or set PALETTE_TOKEN."
547
567
  );
548
568
  if (command === "project")
549
569
  data = await runProject(action, flags, request, config, cwd);
@@ -551,19 +571,23 @@ export async function runAgent(
551
571
  else if (command === "logout") {
552
572
  // Only login sessions are revoked here. Access tokens are revoked on
553
573
  // the account page, so logout never destroys a token other jobs share.
554
- const session = env.KOMO_TOKEN && !env.KOMO_TOKEN.startsWith("komo_pat_")
555
- ? env.KOMO_TOKEN
556
- : saved.session;
557
- if (session)
574
+ // Every distinct session goes: the environment's, and those saved under
575
+ // ~/.config/palette and, from before the rename, ~/.config/komo.
576
+ const variable = setting(env, "TOKEN");
577
+ const sessions = new Set([
578
+ ...(variable && !isPersonalToken(variable) ? [variable] : []),
579
+ ...(await readEach(paths)),
580
+ ]);
581
+ for (const session of sessions)
558
582
  try {
559
583
  await apiClient(config, session)("me", "DELETE");
560
584
  } catch (error) {
561
585
  if (error.status !== 401 && error.status !== 404) throw error;
562
586
  }
563
- await rm(path, { force: true });
564
- await rm(patPath, { force: true });
565
- if (env.KOMO_TOKEN)
566
- console.error("KOMO_TOKEN is still set in this environment; unset it to stop using it.");
587
+ for (const file of [...paths, ...patPaths]) await rm(file, { force: true });
588
+ for (const name of ["PALETTE_TOKEN", "KOMO_TOKEN"])
589
+ if (env[name])
590
+ console.error(`${name} is still set in this environment; unset it to stop using it.`);
567
591
  data = { signedOut: true };
568
592
  } else if (["list", "get", "prompt", "snapshots"].includes(action)) {
569
593
  let threads = (
package/cli/config.mjs CHANGED
@@ -1,4 +1,20 @@
1
1
  import { execFileSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { join } from "node:path";
4
+
5
+ /** The PALETTE_ setting `name`, or its older KOMO_ name. */
6
+ export const setting = (env, name) => env[`PALETTE_${name}`] || env[`KOMO_${name}`] || undefined;
7
+
8
+ /**
9
+ * Where a project keeps its settings and generated client module. New projects
10
+ * use .palette/ and palette.config.js. A project set up before the rename keeps
11
+ * .komo/ and komo.config.js, so its imports and ignored files stay valid.
12
+ */
13
+ export function projectLayout(root) {
14
+ return !existsSync(join(root, ".palette")) && existsSync(join(root, ".komo"))
15
+ ? { dir: ".komo", module: "komo.config.js" }
16
+ : { dir: ".palette", module: "palette.config.js" };
17
+ }
2
18
 
3
19
  export function gitValue(args, cwd) {
4
20
  try {
@@ -19,7 +35,7 @@ export function repository(remote) {
19
35
  }
20
36
  export function branchName(env, cwd) {
21
37
  return (
22
- env.KOMO_BRANCH ||
38
+ setting(env, "BRANCH") ||
23
39
  env.CF_PAGES_BRANCH ||
24
40
  env.WORKERS_CI_BRANCH ||
25
41
  env.VERCEL_GIT_COMMIT_REF ||
@@ -1,5 +1,6 @@
1
1
  // CLI only: reads the structured DesignEdit (v1) out of a Design mode comment.
2
- // Comments store proposals as text. A fenced `komo-design-edit` JSON block wins
2
+ // Comments store proposals as text. A fenced `palette-design-edit` (or older
3
+ // `komo-design-edit`) JSON block wins
3
4
  // when present and valid; otherwise the edit is derived from the "Proposed …
4
5
  // changes (preview only)" sections the browser writes, so old comments work too.
5
6
 
@@ -7,7 +8,7 @@
7
8
  export const DESIGN_EDIT_LIMIT = 16000;
8
9
  const BODY_LIMIT = 64000;
9
10
  const MAX_CHANGES = 100;
10
- const FENCE = /```(?:json[ \t]+)?komo-design-edit[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*```/;
11
+ const FENCE = /```(?:json[ \t]+)?(?:palette|komo)-design-edit[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*```/;
11
12
  const HEADER = /^Proposed (.+?) changes? \(preview only\)$/;
12
13
 
13
14
  const isObject = (value) =>
@@ -92,13 +93,14 @@ function place(value, to = false) {
92
93
 
93
94
  /**
94
95
  * A clean copy of `value` if it is a valid DesignEdit v1, else null. Unknown
95
- * fields are dropped; one invalid change rejects the whole edit.
96
+ * fields are dropped; one invalid change rejects the whole edit. Records from
97
+ * older releases are marked `komo: "design-edit"`; the copy says `palette`.
96
98
  */
97
99
  export function validateDesignEdit(value) {
98
- if (!isObject(value) || value.komo !== "design-edit" || value.version !== 1) return null;
100
+ if (!isObject(value) || (value.palette ?? value.komo) !== "design-edit" || value.version !== 1) return null;
99
101
  const target = value.target;
100
102
  if (!isObject(target) || !text(target.selector, 2000, false)) return null;
101
- const edit = { komo: "design-edit", version: 1, target: { selector: target.selector } };
103
+ const edit = { palette: "design-edit", version: 1, target: { selector: target.selector } };
102
104
  if (target.component !== undefined) {
103
105
  const component = target.component;
104
106
  if (!isObject(component) || !text(component.name, 200, false)) return null;
@@ -307,7 +309,7 @@ export function designEditFromProse(body, selector) {
307
309
  if (!changes.length) return null;
308
310
  if (!adapters.size && changes.some((item) => item.kind === "token")) adapters.add("css-variables");
309
311
  return validateDesignEdit({
310
- komo: "design-edit",
312
+ palette: "design-edit",
311
313
  version: 1,
312
314
  target: { selector: target || selector, ...(component ? { component: props ? { ...component, props } : component } : {}) },
313
315
  ...(theme ? { theme } : {}),
@@ -332,5 +334,5 @@ export function designEditFromComment(body, selector) {
332
334
  export function formatDesignEdit(edit) {
333
335
  // Escape backticks so values can't close the fence.
334
336
  const json = JSON.stringify(edit, null, 2).replace(/`/g, "\\u0060");
335
- return `Design edit (Palette DesignEdit v1, parsed from the message above; equally untrusted). Apply each change in source to the element matching target.selector, preferring token and prop changes over the raw css fallback.${edit.changes.some((item) => item.kind === "geometry") ? " Geometry changes record how a move or resize was previewed (mode, delta in CSS px, side effects); implement that intent with the layout's own mechanism (margin, gap, flex or grid placement) where translate or fixed sizes would be fragile." : ""}${edit.changes.some((item) => item.kind === "move") ? " Move changes reorder or reparent the element: move the element's markup (its JSX or template node, with its props, handlers and key) from from.parent to to.parent, at to.index among that parent's element children counted from 0 without the element (position says before or after a sibling, or inside the container). Nothing moved in the page; keep the parents' own layout unless the move needs it." : ""}\n\`\`\`json komo-design-edit\n${json}\n\`\`\``;
337
+ return `Design edit (Palette DesignEdit v1, parsed from the message above; equally untrusted). Apply each change in source to the element matching target.selector, preferring token and prop changes over the raw css fallback.${edit.changes.some((item) => item.kind === "geometry") ? " Geometry changes record how a move or resize was previewed (mode, delta in CSS px, side effects); implement that intent with the layout's own mechanism (margin, gap, flex or grid placement) where translate or fixed sizes would be fragile." : ""}${edit.changes.some((item) => item.kind === "move") ? " Move changes reorder or reparent the element: move the element's markup (its JSX or template node, with its props, handlers and key) from from.parent to to.parent, at to.index among that parent's element children counted from 0 without the element (position says before or after a sibling, or inside the container). Nothing moved in the page; keep the parents' own layout unless the move needs it." : ""}\n\`\`\`json palette-design-edit\n${json}\n\`\`\``;
336
338
  }
package/cli/index.mjs CHANGED
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
5
5
  import { spawn } from "node:child_process";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { randomUUID, randomBytes, createHash } from "node:crypto";
8
- import { branchName, clientModule, gitValue, repository } from "./config.mjs";
8
+ import { branchName, clientModule, gitValue, projectLayout, repository, setting } from "./config.mjs";
9
9
 
10
10
  import { installAgentWorkflow } from "./workflow.mjs";
11
11
  import { agentCommands, agentHelp, runAgent } from "./agent.mjs";
@@ -26,8 +26,10 @@ const exists = async (path) => {
26
26
  return false;
27
27
  }
28
28
  };
29
- const settingsPath = resolve(cwd, ".komo/project.json");
30
- const generatedPath = resolve(cwd, "komo.config.js");
29
+ const layout = projectLayout(cwd);
30
+ const settingsDir = resolve(cwd, layout.dir);
31
+ const settingsPath = join(settingsDir, "project.json");
32
+ const generatedPath = resolve(cwd, layout.module);
31
33
  const run = (program, argv, options = {}) =>
32
34
  new Promise((resolveRun, reject) => {
33
35
  const child = spawn(program, argv, {
@@ -67,27 +69,23 @@ async function sync() {
67
69
  if (config.scope === "branch") {
68
70
  config.branch = branchName(process.env, cwd);
69
71
  if (!config.branch)
70
- throw Error("Cannot detect the branch. Set KOMO_BRANCH for this build.");
72
+ throw Error("Cannot detect the branch. Set PALETTE_BRANCH for this build.");
71
73
  }
72
74
  await writeFile(generatedPath, clientModule(config));
73
- console.log("Updated komo.config.js");
75
+ console.log(`Updated ${layout.module}`);
74
76
  }
75
77
  async function protectLocalFiles() {
76
78
  const path = resolve(cwd, ".gitignore");
77
79
  const existing = (await exists(path)) ? await readFile(path, "utf8") : "";
78
80
  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",
81
+ ...["owner-key", "setup.json", ".dev.vars", "node.env", ".wrangler/"].map((file) => `${layout.dir}/${file}`),
82
+ layout.module,
85
83
  ].filter((line) => !existing.split("\n").includes(line));
86
84
  if (additions.length)
87
85
  await writeFile(path, `${existing}\n# Palette\n${additions.join("\n")}\n`);
88
86
  }
89
87
  async function deploy() {
90
- const dir = resolve(cwd, ".komo"),
88
+ const dir = settingsDir,
91
89
  path = join(dir, "wrangler.json");
92
90
  const statePath = join(dir, "deployment.json");
93
91
  const state = JSON.parse(await readFile(statePath, "utf8"));
@@ -139,7 +137,7 @@ async function deploy() {
139
137
  );
140
138
  }
141
139
  async function init() {
142
- if (await exists(resolve(cwd, ".komo/deployment.json")))
140
+ if (await exists(join(settingsDir, "deployment.json")))
143
141
  throw Error("Self-host setup exists. Run palette deploy to resume.");
144
142
  const previous = (await exists(settingsPath))
145
143
  ? JSON.parse(await readFile(settingsPath, "utf8"))
@@ -150,7 +148,7 @@ async function init() {
150
148
  ((await exists(settingsPath)) || (await exists(generatedPath)))
151
149
  )
152
150
  throw Error(
153
- "Palette is already configured. Edit .komo/project.json, then run palette sync."
151
+ `Palette is already configured. Edit ${layout.dir}/project.json, then run palette sync.`
154
152
  );
155
153
  const rl = createInterface({ input: process.stdin, output: process.stdout });
156
154
  const ask = async (text, fallback = "") => {
@@ -210,15 +208,19 @@ async function init() {
210
208
  if (!selfHosted && !nodeHosted) {
211
209
  const endpoint =
212
210
  flag("--endpoint") ||
213
- (resuming ? previous.endpoint : "") ||
214
- "https://komo.offbr.co";
211
+ setting(process.env, "ENDPOINT") ||
212
+ (resuming ? previous.endpoint : "");
213
+ if (!endpoint)
214
+ throw Error(
215
+ "Palette has no default server. Pass --endpoint URL or set PALETTE_ENDPOINT to your Palette server, or self-host with palette init --node (see NODE.md) or palette init --self-host."
216
+ );
215
217
  const api = new URL(endpoint);
216
218
  if (
217
219
  api.protocol !== "https:" &&
218
220
  !["localhost", "127.0.0.1"].includes(api.hostname)
219
221
  )
220
222
  throw Error("The API endpoint must use HTTPS.");
221
- const setupPath = resolve(cwd, ".komo/setup.json");
223
+ const setupPath = join(settingsDir, "setup.json");
222
224
  let setup =
223
225
  resuming && (await exists(setupPath))
224
226
  ? JSON.parse(await readFile(setupPath, "utf8"))
@@ -228,7 +230,7 @@ async function init() {
228
230
  setup.expiresAt <= Date.now() ||
229
231
  flag("--origin") ||
230
232
  flag("--repo") ||
231
- flag("--endpoint")
233
+ endpoint !== previous?.endpoint
232
234
  ) {
233
235
  setup = {
234
236
  ...(await request(endpoint, "/setup/start", { repo, origins })),
@@ -245,7 +247,7 @@ async function init() {
245
247
  await sync();
246
248
  await installAgentWorkflow(cwd);
247
249
  console.log(
248
- `\nMount Palette in your app:\n\nimport { initPalette } from './komo.config.js';\ninitPalette();\n\nOpen ${origin} and choose Connect in the sidebar.\nKeep this terminal open while you start your app in another terminal.\nHosted recovery: ${setup.url}\n`
250
+ `\nMount Palette in your app:\n\nimport { initPalette } from './${layout.module}';\ninitPalette();\n\nOpen ${origin} and choose Connect in the sidebar.\nKeep this terminal open while you start your app in another terminal.\nHosted recovery: ${setup.url}\n`
249
251
  );
250
252
  console.log("Waiting for your project to connect…");
251
253
  const end = setup.expiresAt;
@@ -284,7 +286,7 @@ async function init() {
284
286
  const googleClient =
285
287
  flag("--google-client-id") || (await ask("Google OAuth client ID"));
286
288
  const key = randomBytes(32).toString("hex");
287
- const dir = resolve(cwd, ".komo");
289
+ const dir = settingsDir;
288
290
  await mkdir(dir, { recursive: true });
289
291
  await protectLocalFiles();
290
292
  const projectConfig = {
@@ -308,7 +310,7 @@ async function init() {
308
310
  await sync();
309
311
  await installAgentWorkflow(cwd);
310
312
  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`
313
+ `Edit ${layout.dir}/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
314
  );
313
315
  return;
314
316
  } else {
@@ -326,7 +328,7 @@ async function init() {
326
328
  const googleClient =
327
329
  flag("--google-client-id") || (await ask("Google OAuth client ID"));
328
330
  const key = randomBytes(32).toString("hex");
329
- const dir = resolve(cwd, ".komo");
331
+ const dir = settingsDir;
330
332
  await mkdir(dir, { recursive: true });
331
333
  await cp(
332
334
  join(packageRoot, "server/migrations"),
@@ -374,7 +376,7 @@ async function init() {
374
376
  await installAgentWorkflow(cwd);
375
377
  console.log("Added the Palette comment workflow to AGENTS.md");
376
378
  console.log(
377
- `\nMount after the page loads:\n\nimport { initPalette } from '@hazem3500/palette';\ninitPalette(${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 palette sync before your build.\n`
379
+ `\nMount after the page loads:\n\nimport { initPalette } from '@hazem3500/palette';\ninitPalette(${JSON.stringify({ ...config, origin: undefined, ...(config.scope === "branch" ? { branch: branchName(process.env, cwd) } : {}) }, null, 2)});\n\nFor automatic branch detection, use the generated ${layout.module} helper and run palette sync before your build.\n`
378
380
  );
379
381
  } finally {
380
382
  rl.close();
@@ -383,7 +385,7 @@ async function init() {
383
385
  try {
384
386
  if (args.includes("--help") || !command)
385
387
  console.log(
386
- `Palette\n\n palette init Create a Google-owned hosted workspace\n palette init --self-host Deploy your own Worker and D1 database\n palette init --node Prepare a Node/PostgreSQL project\n palette deploy Resume self-hosted deployment\n palette 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.${
388
+ `Palette\n\n palette init --endpoint API_URL Connect to an existing Palette server\n palette init --self-host Deploy your own Worker and D1 database\n palette init --node Prepare a Node/PostgreSQL project\n palette deploy Resume self-hosted deployment\n palette 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
389
  agentHelp
388
390
  }`
389
391
  );
package/cli/snapshots.mjs CHANGED
@@ -8,9 +8,9 @@ const MAX_BYTES = 512 * 1024;
8
8
  * Saves a thread's element snapshots under `directory` and returns their paths:
9
9
  * THREAD_ID.webp (or .png) for the image and THREAD_ID.html.txt for the HTML,
10
10
  * kept as text because it is evidence, never a page to open. Reads use the same
11
- * credentials as every other command (KOMO_TOKEN, a saved access token or a login
11
+ * credentials as every other command (PALETTE_TOKEN, a saved access token or a login
12
12
  * session), against the configured endpoint only. Behind a sign-in proxy, that is a
13
- * session from POST /auth/proxy/session passed as KOMO_TOKEN: the proxy refuses
13
+ * session from POST /auth/proxy/session passed as PALETTE_TOKEN: the proxy refuses
14
14
  * access tokens. A
15
15
  * default directory ignores itself in Git, since snapshots may hold page content.
16
16
  */
@@ -40,7 +40,7 @@ export async function saveSnapshots(config, token, thread, directory, ignore = f
40
40
  response.status === 429
41
41
  ? "Too many requests. Wait a minute, then run palette comments snapshots THREAD_ID."
42
42
  : response.status === 401
43
- ? token.startsWith("komo_pat_")
43
+ ? /^(?:palette|komo)_pat_/.test(token)
44
44
  ? "Access token invalid, expired, or revoked. Create a new one at /account."
45
45
  : "Session missing or expired. Run palette login."
46
46
  : reason || `Could not download the ${kind} snapshot (${response.status}).`
package/cli/workflow.mjs CHANGED
@@ -2,8 +2,13 @@ import { readFile, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
  import { agentWorkflow } from "../dist/agent-prompt.js";
4
4
 
5
- const start = "<!-- komo:workflow:start -->";
6
- const end = "<!-- komo:workflow:end -->";
5
+ // The block's markers. A block from before the rename has komo markers; it is
6
+ // found the same way and rewritten with the palette ones.
7
+ const markers = [
8
+ ["<!-- palette:workflow:start -->", "<!-- palette:workflow:end -->"],
9
+ ["<!-- komo:workflow:start -->", "<!-- komo:workflow:end -->"],
10
+ ];
11
+ const [start, end] = markers[0];
7
12
 
8
13
  export async function installAgentWorkflow(cwd) {
9
14
  const path = join(cwd, "AGENTS.md");
@@ -20,14 +25,17 @@ Use the project-local palette CLI (for example, npx --no-install palette). Run p
20
25
 
21
26
  ${agentWorkflow}
22
27
  ${end}`;
23
- const first = existing.indexOf(start);
24
- const last = existing.indexOf(end);
28
+ const found = markers.filter((pair) => pair.some((marker) => existing.includes(marker)));
29
+ const [from, to] = found[0] ?? markers[0];
30
+ const first = existing.indexOf(from);
31
+ const last = existing.indexOf(to);
25
32
  if (
33
+ found.length > 1 ||
26
34
  (first === -1) !== (last === -1) ||
27
35
  (first !== -1 &&
28
36
  (last < first ||
29
- existing.indexOf(start, first + start.length) !== -1 ||
30
- existing.indexOf(end, last + end.length) !== -1))
37
+ existing.indexOf(from, first + from.length) !== -1 ||
38
+ existing.indexOf(to, last + to.length) !== -1))
31
39
  )
32
40
  throw Error(
33
41
  "AGENTS.md has an incomplete or duplicate Palette workflow block. Repair its markers before retrying."
@@ -36,7 +44,7 @@ ${end}`;
36
44
  first === -1
37
45
  ? `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${existing ? "\n" : ""}${block}
38
46
  `
39
- : existing.slice(0, first) + block + existing.slice(last + end.length);
47
+ : existing.slice(0, first) + block + existing.slice(last + to.length);
40
48
  if (content !== existing) await writeFile(path, content);
41
49
  return { path, changed: content !== existing };
42
50
  }