ponzu 1.68.2 → 1.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6978,7 +6978,7 @@ var require_dist = __commonJS(function(exports, module) {
6978
6978
  // apps/cli/src/index.ts
6979
6979
  import { existsSync as existsSync13 } from "fs";
6980
6980
  import { networkInterfaces } from "os";
6981
- import { dirname as dirname11, isAbsolute as isAbsolute4, join as join19, resolve as resolve5 } from "path";
6981
+ import { dirname as dirname12, isAbsolute as isAbsolute4, join as join20, resolve as resolve5 } from "path";
6982
6982
  import { fileURLToPath as fileURLToPath2 } from "url";
6983
6983
  import { parseArgs } from "util";
6984
6984
 
@@ -16498,6 +16498,23 @@ function carryIds(prev, next, derived) {
16498
16498
  }
16499
16499
  return changed;
16500
16500
  }
16501
+ // packages/core/src/suggestions.ts
16502
+ function sameName(a, b) {
16503
+ const norm = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
16504
+ const x = norm(a);
16505
+ return x.length > 0 && x === norm(b);
16506
+ }
16507
+ function startedAs(idea, file) {
16508
+ const folder = file.key.split("/").pop()?.replace(/\.design$/, "") ?? "";
16509
+ return sameName(idea.name, file.name) || sameName(idea.name, folder);
16510
+ }
16511
+ var STARTED_FOR = 10 * 60000;
16512
+ function startedFrom(idea, files) {
16513
+ return files.find((f) => f.git?.project === idea.projectId && startedAs(idea, f));
16514
+ }
16515
+ function settled(file, now = Date.now()) {
16516
+ return !file.busy && !(file.created && now - file.created < STARTED_FOR);
16517
+ }
16501
16518
  // packages/core/src/update.ts
16502
16519
  function releaseNotesUrl(version) {
16503
16520
  return `https://github.com/north-brook/ponzu/releases/download/v${version}/release-notes.json`;
@@ -41722,8 +41739,8 @@ async function previewStale(root) {
41722
41739
  }
41723
41740
  // packages/server/src/serve.ts
41724
41741
  import { existsSync as existsSync8, statSync as statSync2 } from "fs";
41725
- import { readFile as readFile9 } from "fs/promises";
41726
- import { extname, join as join12, normalize, resolve as resolve4, sep as sep3 } from "path";
41742
+ import { readFile as readFile10 } from "fs/promises";
41743
+ import { extname, join as join13, normalize, resolve as resolve4, sep as sep3 } from "path";
41727
41744
 
41728
41745
  // packages/store/src/fs.ts
41729
41746
  import { watch as fsWatch, openAsBlob } from "fs";
@@ -43175,7 +43192,29 @@ function isListed(key) {
43175
43192
  return !/\/(node_modules|\.Trash|\.cache)\//.test(key) && !/\/\.[^/]+\.design$/.test(key);
43176
43193
  }
43177
43194
  async function gitOf(key) {
43178
- for (let dir = key;; dir = dirname4(dir)) {
43195
+ const found = await checkoutOf(key);
43196
+ if (!found)
43197
+ return null;
43198
+ const { root, gitdir, common } = found;
43199
+ const at = { root, worktree: common !== gitdir, ...await projectOf(common) };
43200
+ const head = (await readFile4(join6(gitdir, "HEAD"), "utf8")).trim();
43201
+ const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
43202
+ if (ref)
43203
+ return { ...at, branch: ref[1] };
43204
+ return /^[0-9a-f]{7,}$/.test(head) ? { ...at, commit: head.slice(0, 7) } : at;
43205
+ }
43206
+ async function repositoryOf(dir) {
43207
+ const found = await checkoutOf(dir);
43208
+ if (!found)
43209
+ return null;
43210
+ const root = basename(found.common) === ".git" ? dirname4(found.common) : null;
43211
+ if (!root || !await exists(root))
43212
+ return null;
43213
+ const { project, projectName } = await projectOf(found.common);
43214
+ return { root, project: project ?? root, name: projectName ?? basename(root) };
43215
+ }
43216
+ async function checkoutOf(dir) {
43217
+ for (;; dir = dirname4(dir)) {
43179
43218
  const dotgit = join6(dir, ".git");
43180
43219
  const s = await stat5(dotgit).catch(() => null);
43181
43220
  if (s) {
@@ -43188,12 +43227,7 @@ async function gitOf(key) {
43188
43227
  }
43189
43228
  const commondir = await readFile4(join6(gitdir, "commondir"), "utf8").catch(() => null);
43190
43229
  const common = commondir === null ? gitdir : resolve2(gitdir, commondir.trim());
43191
- const at = { root: dir, worktree: commondir !== null, ...await projectOf(common) };
43192
- const head = (await readFile4(join6(gitdir, "HEAD"), "utf8")).trim();
43193
- const ref = /^ref:\s*refs\/heads\/(.+)$/.exec(head);
43194
- if (ref)
43195
- return { ...at, branch: ref[1] };
43196
- return /^[0-9a-f]{7,}$/.test(head) ? { ...at, commit: head.slice(0, 7) } : at;
43230
+ return { root: dir, gitdir, common };
43197
43231
  }
43198
43232
  if (dirname4(dir) === dir)
43199
43233
  return null;
@@ -46024,6 +46058,731 @@ var RASTER = new Set([
46024
46058
  "image/avif"
46025
46059
  ]);
46026
46060
 
46061
+ // packages/server/src/suggest.ts
46062
+ import { spawn as spawn5 } from "child_process";
46063
+ import { createReadStream } from "fs";
46064
+ import {
46065
+ mkdir as mkdir10,
46066
+ mkdtemp,
46067
+ readdir as readdir6,
46068
+ readFile as readFile9,
46069
+ realpath as realpath2,
46070
+ rename as rename8,
46071
+ rm as rm4,
46072
+ stat as stat9,
46073
+ writeFile as writeFile8
46074
+ } from "fs/promises";
46075
+ import { homedir as homedir4, tmpdir as tmpdir2 } from "os";
46076
+ import { basename as basename3, dirname as dirname5, join as join12 } from "path";
46077
+ var PROJECTS = 5;
46078
+ var IDEAS = 8;
46079
+ var PER_PROJECT = 4;
46080
+ var KEEP = 16;
46081
+ var STALE = 7 * 24 * 60 * 60000;
46082
+ var SESSIONS = 200;
46083
+ var HEAD_BYTES = 64 * 1024;
46084
+ var AT_ONCE = 8;
46085
+ var RUN_EVERY = 10 * 60000;
46086
+ var RUN_TIMEOUT = 3 * 60000;
46087
+ var QUIET = 20000;
46088
+ var TRIES = 2;
46089
+ var STEPS = 4;
46090
+ var LISTED = 600;
46091
+ var NOTE_BYTES = 4000;
46092
+ var GLANCE_SCHEMA = {
46093
+ type: "object",
46094
+ additionalProperties: false,
46095
+ required: ["ideas"],
46096
+ properties: {
46097
+ ideas: {
46098
+ type: "array",
46099
+ items: {
46100
+ type: "object",
46101
+ additionalProperties: false,
46102
+ required: ["name", "files"],
46103
+ properties: {
46104
+ name: { type: "string" },
46105
+ files: { type: "array", items: { type: "string" } }
46106
+ }
46107
+ }
46108
+ }
46109
+ }
46110
+ };
46111
+ var PROMPT_SCHEMA = {
46112
+ type: "object",
46113
+ additionalProperties: false,
46114
+ required: ["prompt"],
46115
+ properties: { prompt: { type: "string" } }
46116
+ };
46117
+
46118
+ class SuggestionService {
46119
+ opts;
46120
+ ideas = [];
46121
+ looking = [];
46122
+ ran = 0;
46123
+ running = false;
46124
+ stopped = new AbortController;
46125
+ loaded;
46126
+ saving = Promise.resolve();
46127
+ every;
46128
+ now;
46129
+ constructor(opts) {
46130
+ this.opts = opts;
46131
+ this.every = opts.every ?? RUN_EVERY;
46132
+ this.now = opts.now ?? Date.now;
46133
+ this.loaded = this.load();
46134
+ }
46135
+ async state() {
46136
+ await this.loaded;
46137
+ const [agent, designs] = await Promise.all([this.agent(), this.designs()]);
46138
+ const now = this.now();
46139
+ return {
46140
+ agent: agent?.id ?? null,
46141
+ ideas: this.ideas.filter((i) => {
46142
+ const started = startedFrom(i, designs);
46143
+ return !started || !settled(started, now);
46144
+ }),
46145
+ running: this.running,
46146
+ looking: this.looking
46147
+ };
46148
+ }
46149
+ async refresh() {
46150
+ await this.loaded;
46151
+ const due = () => !this.running && this.now() - this.ran >= this.every;
46152
+ if (due()) {
46153
+ const agent = await this.agent();
46154
+ if (agent && due()) {
46155
+ this.running = true;
46156
+ this.ran = this.now();
46157
+ this.run(agent).finally(() => {
46158
+ this.running = false;
46159
+ this.looking = [];
46160
+ });
46161
+ }
46162
+ }
46163
+ return this.state();
46164
+ }
46165
+ stop() {
46166
+ this.stopped.abort();
46167
+ }
46168
+ async agent() {
46169
+ const listed = this.opts.agents ? await this.opts.agents().catch(() => []) : null;
46170
+ const isolated = this.opts.isolated ?? (this.opts.run ? null : isolates);
46171
+ for (const id of ["claude", "codex"]) {
46172
+ if (listed && !listed.some((a) => a.id === id && a.found))
46173
+ continue;
46174
+ const bin = this.opts.bin(id);
46175
+ if (!bin)
46176
+ continue;
46177
+ if (isolated && !await isolated(id, bin, this.env()).catch(() => false))
46178
+ continue;
46179
+ return { id, bin };
46180
+ }
46181
+ return null;
46182
+ }
46183
+ env() {
46184
+ return { ...process.env, ...this.opts.env };
46185
+ }
46186
+ async designs() {
46187
+ return await this.opts.designs?.().catch(() => []) ?? [];
46188
+ }
46189
+ job(agent, project, kind, prompt, more = {}) {
46190
+ return {
46191
+ agent: agent.id,
46192
+ bin: agent.bin,
46193
+ kind,
46194
+ project,
46195
+ prompt,
46196
+ schema: kind === "glance" ? GLANCE_SCHEMA : PROMPT_SCHEMA,
46197
+ signal: AbortSignal.any([
46198
+ this.stopped.signal,
46199
+ AbortSignal.timeout(this.opts.timeout ?? RUN_TIMEOUT)
46200
+ ]),
46201
+ env: this.env(),
46202
+ ...more
46203
+ };
46204
+ }
46205
+ async run(agent) {
46206
+ await this.save();
46207
+ const projects = await recentProjects({ home: this.opts.home }).catch(() => []);
46208
+ if (!projects.length)
46209
+ return;
46210
+ const designs = await this.designs();
46211
+ const offered = this.ideas;
46212
+ const now = this.now();
46213
+ this.ideas = this.ideas.filter((i) => startedFrom(i, designs) || now - i.found < STALE);
46214
+ const open = this.ideas.filter((i) => !startedFrom(i, designs)).length;
46215
+ const counts = shares(IDEAS - open, projects.length);
46216
+ const home = this.opts.home;
46217
+ this.looking = projects.filter((_, i) => counts[i] > 0).map((p) => lookingAt(p, home));
46218
+ const runner = this.opts.run ?? runAgent;
46219
+ const lane = limiter(AT_ONCE);
46220
+ const writing = [];
46221
+ await Promise.all(projects.map(async (project, i) => {
46222
+ const count = counts[i];
46223
+ if (!count || this.stopped.signal.aborted)
46224
+ return;
46225
+ try {
46226
+ const taken = {
46227
+ designs: designs.filter((d) => d.git?.project === project.project).map((d) => d.name),
46228
+ suggested: offered.filter((i) => i.project === project.root).map((i) => i.name)
46229
+ };
46230
+ const listing = await listingOf(project.root, this.env());
46231
+ const ask = glanceFor(project, count, taken, listing, home);
46232
+ const answer = await lane(() => runner(this.job(agent, project, "glance", ask)));
46233
+ const found = glanceFrom(answer, project, { count, found: this.now() }).filter(({ idea }) => !startedFrom(idea, designs));
46234
+ if (!found.length)
46235
+ return;
46236
+ this.ideas = [
46237
+ ...found.map((f) => f.idea),
46238
+ ...this.ideas.filter((old) => !found.some((f) => sameIdea(f.idea, old)))
46239
+ ].slice(0, KEEP);
46240
+ for (const f of found)
46241
+ writing.push(this.write(agent, project, f.idea, f.files, lane));
46242
+ } catch (err) {
46243
+ console.error(`Suggestions for ${project.name}: ${err.message ?? err}`);
46244
+ } finally {
46245
+ this.looking = this.looking.filter((l) => l.project !== project.root);
46246
+ }
46247
+ }));
46248
+ await Promise.all(writing);
46249
+ }
46250
+ async write(agent, project, idea, files, lane) {
46251
+ const ask = promptFor(idea, files, project, this.opts.home);
46252
+ const runner = this.opts.run ?? runAgent;
46253
+ const onStep = (step) => {
46254
+ idea.steps = [...idea.steps ?? [], step].slice(-STEPS);
46255
+ };
46256
+ let deadline;
46257
+ for (let tries = 0;tries < TRIES && !this.stopped.signal.aborted; tries++) {
46258
+ if (deadline?.aborted)
46259
+ break;
46260
+ idea.steps = [];
46261
+ try {
46262
+ const answer = await lane(() => {
46263
+ deadline ??= AbortSignal.timeout(this.opts.timeout ?? RUN_TIMEOUT);
46264
+ const signal = AbortSignal.any([this.stopped.signal, deadline]);
46265
+ return runner(this.job(agent, project, "prompt", ask, { onStep, quiet: QUIET, signal }));
46266
+ });
46267
+ const prompt = promptFrom(answer, idea, project, this.opts.home);
46268
+ if (!prompt)
46269
+ continue;
46270
+ idea.prompt = prompt;
46271
+ delete idea.steps;
46272
+ await this.save();
46273
+ return;
46274
+ } catch (err) {
46275
+ console.error(`Suggestion \u201C${idea.name}\u201D: ${err.message ?? err}`);
46276
+ }
46277
+ }
46278
+ this.ideas = this.ideas.filter((i) => i !== idea);
46279
+ }
46280
+ async load() {
46281
+ if (!this.opts.file)
46282
+ return;
46283
+ try {
46284
+ const saved = JSON.parse(await readFile9(this.opts.file, "utf8"));
46285
+ this.ran = typeof saved.ran === "number" ? saved.ran : 0;
46286
+ this.ideas = Array.isArray(saved.ideas) ? saved.ideas.filter((i) => isIdea(i) && i.prompt) : [];
46287
+ } catch {}
46288
+ }
46289
+ save() {
46290
+ const file = this.opts.file;
46291
+ if (!file)
46292
+ return Promise.resolve();
46293
+ this.saving = this.saving.then(async () => {
46294
+ const ideas = this.ideas.filter((i) => i.prompt).map(({ steps: _, ...i }) => i);
46295
+ const saved = { ran: this.ran, ideas };
46296
+ try {
46297
+ await mkdir10(dirname5(file), { recursive: true });
46298
+ const tmp = `${file}.${process.pid}.tmp`;
46299
+ await writeFile8(tmp, `${JSON.stringify(saved, null, 2)}
46300
+ `);
46301
+ await rename8(tmp, file);
46302
+ } catch {}
46303
+ });
46304
+ return this.saving;
46305
+ }
46306
+ }
46307
+ function sameIdea(a, b) {
46308
+ return a.project === b.project && startedAs(a, { name: b.name, key: b.name });
46309
+ }
46310
+ function isIdea(i) {
46311
+ const o = i;
46312
+ return typeof o?.name === "string" && typeof o.project === "string" && typeof o.projectId === "string" && typeof o.projectName === "string" && typeof o.prompt === "string" && typeof o.found === "number";
46313
+ }
46314
+ function lookingAt(p, home) {
46315
+ return {
46316
+ project: p.root,
46317
+ projectId: p.project,
46318
+ projectName: p.name,
46319
+ folder: tilde(p.root, home)
46320
+ };
46321
+ }
46322
+ function shares(total, n) {
46323
+ return Array.from({ length: n }, (_, i) => Math.min(PER_PROJECT, Math.max(0, Math.floor(total / n) + (i < total % n ? 1 : 0))));
46324
+ }
46325
+ function limiter(n) {
46326
+ let running = 0;
46327
+ const waiting = [];
46328
+ return async (fn) => {
46329
+ if (running < n)
46330
+ running++;
46331
+ else
46332
+ await new Promise((go) => waiting.push(go));
46333
+ try {
46334
+ return await fn();
46335
+ } finally {
46336
+ const next = waiting.shift();
46337
+ if (next)
46338
+ next();
46339
+ else
46340
+ running--;
46341
+ }
46342
+ };
46343
+ }
46344
+ async function recentProjects(o = {}) {
46345
+ const home = o.home ?? homedir4();
46346
+ const env = o.env ?? (o.home ? {} : process.env);
46347
+ const limit = o.limit ?? PROJECTS;
46348
+ const sessions = [
46349
+ ...await claudeSessions(env.CLAUDE_CONFIG_DIR ?? join12(home, ".claude")),
46350
+ ...await codexSessions(env.CODEX_HOME ?? join12(home, ".codex"))
46351
+ ].sort((a, b) => b.at - a.at);
46352
+ const out = new Map;
46353
+ const tried = new Set;
46354
+ for (const s of sessions.slice(0, SESSIONS)) {
46355
+ if (out.size >= limit)
46356
+ break;
46357
+ const cwd = await cwdOf(s.file);
46358
+ if (!cwd || tried.has(cwd))
46359
+ continue;
46360
+ tried.add(cwd);
46361
+ if (cwd === home || !cwd.startsWith(`${home}/`) && isTemporary(cwd))
46362
+ continue;
46363
+ if (!await stat9(cwd).then((st) => st.isDirectory(), () => false))
46364
+ continue;
46365
+ const repo = await repositoryOf(cwd).catch(() => null);
46366
+ if (!repo || repo.root === home || out.has(repo.project))
46367
+ continue;
46368
+ out.set(repo.project, {
46369
+ root: repo.root,
46370
+ name: basename3(repo.root),
46371
+ project: repo.project,
46372
+ at: s.at
46373
+ });
46374
+ }
46375
+ return [...out.values()];
46376
+ }
46377
+ async function claudeSessions(dir) {
46378
+ const folders = await readdir6(join12(dir, "projects")).catch(() => []);
46379
+ const out = [];
46380
+ await Promise.all(folders.map(async (f) => {
46381
+ const at = join12(dir, "projects", f);
46382
+ const files = await readdir6(at).catch(() => []);
46383
+ for (const name of files) {
46384
+ if (!name.endsWith(".jsonl"))
46385
+ continue;
46386
+ const file = join12(at, name);
46387
+ const s = await stat9(file).catch(() => null);
46388
+ if (s?.isFile())
46389
+ out.push({ file, at: s.mtimeMs });
46390
+ }
46391
+ }));
46392
+ return out;
46393
+ }
46394
+ async function codexSessions(dir) {
46395
+ const files = [];
46396
+ const walk = async (at, depth) => {
46397
+ const names = (await readdir6(at).catch(() => [])).sort().reverse();
46398
+ for (const name of names) {
46399
+ if (files.length >= SESSIONS)
46400
+ return;
46401
+ const path = join12(at, name);
46402
+ if (depth < 3)
46403
+ await walk(path, depth + 1);
46404
+ else if (name.endsWith(".jsonl"))
46405
+ files.push(path);
46406
+ }
46407
+ };
46408
+ await walk(join12(dir, "sessions"), 0);
46409
+ const out = [];
46410
+ for (const file of files) {
46411
+ const s = await stat9(file).catch(() => null);
46412
+ if (s?.isFile())
46413
+ out.push({ file, at: s.mtimeMs });
46414
+ }
46415
+ return out;
46416
+ }
46417
+ var CWD = /"cwd"\s*:\s*("(?:[^"\\]|\\.)*")/;
46418
+ async function cwdOf(file) {
46419
+ const head = await new Promise((done) => {
46420
+ let text = "";
46421
+ const stream = createReadStream(file, { encoding: "utf8", start: 0, end: HEAD_BYTES - 1 });
46422
+ stream.on("data", (chunk) => {
46423
+ text += chunk;
46424
+ if (CWD.test(text))
46425
+ stream.destroy();
46426
+ });
46427
+ stream.on("close", () => done(text));
46428
+ stream.on("error", () => done(text));
46429
+ });
46430
+ const m = CWD.exec(head);
46431
+ if (!m)
46432
+ return null;
46433
+ try {
46434
+ const cwd = JSON.parse(m[1]);
46435
+ return cwd.startsWith("/") ? cwd.replace(/\/+$/, "") || "/" : null;
46436
+ } catch {
46437
+ return null;
46438
+ }
46439
+ }
46440
+ function isTemporary(dir) {
46441
+ return [tmpdir2(), "/tmp", "/private/tmp", "/var/folders", "/private/var/folders"].some((t) => dir === t || dir.startsWith(`${t.replace(/\/+$/, "")}/`));
46442
+ }
46443
+ function tilde(path, home = homedir4()) {
46444
+ return path === home ? "~" : path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
46445
+ }
46446
+ var UNLISTED_DIR = /(^|\/)(node_modules|dist|build|out|\.next|vendor|Pods)\//;
46447
+ var UNLISTED_FILE = /\.(png|jpe?g|gif|svg|ico|icns|webp|avif|mp4|mov|woff2?|ttf|otf|lock|snap)$/i;
46448
+ var NOTES = ["README.md", "AGENTS.md", "CLAUDE.md"];
46449
+ async function listingOf(root, env) {
46450
+ const out = await exec("git", ["-C", root, "ls-files", "--cached", "--others", "--exclude-standard"], { env, signal: AbortSignal.timeout(15000) }).catch(() => "");
46451
+ const all = out.split(`
46452
+ `).filter((f) => f && !UNLISTED_DIR.test(f) && !UNLISTED_FILE.test(f));
46453
+ const files = all.length > LISTED ? all.filter((f) => f.split("/").length <= 5).slice(0, LISTED) : all;
46454
+ const inside = await realpath2(root).catch(() => root);
46455
+ const notes = await Promise.all(NOTES.map(async (name) => {
46456
+ const at = await realpath2(join12(root, name)).catch(() => null);
46457
+ if (!at?.startsWith(`${inside}/`))
46458
+ return "";
46459
+ const text = await readFile9(at, "utf8").catch(() => "");
46460
+ return text.trim() ? `${name}:
46461
+ ${text.slice(0, NOTE_BYTES).trim()}` : "";
46462
+ }));
46463
+ return { files, count: all.length, notes: notes.filter(Boolean).join(`
46464
+
46465
+ `) };
46466
+ }
46467
+ function glanceFor(p, count, taken, listing, home) {
46468
+ const folder = tilde(p.root, home);
46469
+ const names = (list) => list.map((n) => `\u201C${n}\u201D`).join(", ");
46470
+ const ideas = count === 1 ? "one design" : `${count} designs`;
46471
+ return [
46472
+ `You're suggesting designs for Ponzu, a design canvas where the person's coding agent draws in HTML and CSS and they steer it with comments on the canvas.`,
46473
+ ``,
46474
+ `From the repository in ${folder}, listed below, suggest ${ideas} they might ask their agent for next. Each must start from something really in it, named the way the code names it: its screens and their states, a flow end to end, a component, the data model, how its parts fit together, its brand or docs, a page it's missing. Prefer what someone working on it would want to see laid out, or decide between a few takes on.`,
46475
+ ``,
46476
+ `For each one give:`,
46477
+ `- name: what the design is called, like a file name, in sentence case and two to five words, e.g. "Every Seekle screen" or "Checkout, three takes".`,
46478
+ `- files: two to four paths from the list that it starts from.`,
46479
+ ``,
46480
+ `Leave out branches, worktrees and time estimates.${taken.designs.length ? ` It already has these designs, so suggest others: ${names(taken.designs)}.` : ""}${taken.suggested.length ? ` These were suggested before; suggest different ones, not new takes on them: ${names(taken.suggested)}.` : ""}`,
46481
+ `Use no tools and change nothing: everything you need is here. Answer with the JSON alone: {"ideas": [{"name", "files"}]}.`,
46482
+ ``,
46483
+ `<files count="${listing.count}">`,
46484
+ ...listing.files,
46485
+ `</files>`,
46486
+ ...listing.notes ? [``, `<notes>`, listing.notes, `</notes>`] : []
46487
+ ].join(`
46488
+ `);
46489
+ }
46490
+ function promptFor(idea, files, p, home) {
46491
+ const folder = tilde(p.root, home);
46492
+ return [
46493
+ `You're writing a prompt for Ponzu, a design canvas where the person's coding agent draws in HTML and CSS and they steer it with comments on the canvas. They'll send it to their agent to make a design called \u201C${idea.name}\u201D in ${folder}${files.length ? `, starting from ${files.join(", ")}` : ""}. Read the code it should start from, a few files at most, then write it.`,
46494
+ ``,
46495
+ `Its card shows the prompt, so it also teaches them how to ask for a design: write it in their voice, the way they'd type it, and be specific. Ask for what Ponzu is good at: each screen or state on its own artboard, side by side; a few takes to compare; a flow or a diagram drawn from the code; and their comments on the canvas to steer it.`,
46496
+ ``,
46497
+ `Start it with: In ${folder}, make a new Ponzu design called \u201C${idea.name}\u201D. Then, in three to six sentences, ask for what to draw, name the files, screens and states in the code to start from, and say how to lay it out on the canvas. End by asking it to watch the canvas for their comments. Write plain text, no Markdown, and leave out branches, worktrees and time estimates.`,
46498
+ ``,
46499
+ `Only read: change nothing. Answer with the JSON alone: {"prompt": "\u2026"}.`
46500
+ ].join(`
46501
+ `);
46502
+ }
46503
+ function glanceFrom(answer, p, o) {
46504
+ const list = answer?.ideas;
46505
+ if (!Array.isArray(list))
46506
+ return [];
46507
+ const out = [];
46508
+ for (const raw of list) {
46509
+ const r = raw;
46510
+ const name = clean(r?.name);
46511
+ if (!name || name.length > 80)
46512
+ continue;
46513
+ if (out.some(({ idea }) => startedAs(idea, { name, key: name })))
46514
+ continue;
46515
+ const files = (Array.isArray(r.files) ? r.files : []).map(clean).filter(Boolean).slice(0, 4);
46516
+ out.push({
46517
+ idea: {
46518
+ name,
46519
+ project: p.root,
46520
+ projectId: p.project,
46521
+ projectName: p.name,
46522
+ prompt: "",
46523
+ steps: [],
46524
+ found: o.found
46525
+ },
46526
+ files
46527
+ });
46528
+ if (out.length >= o.count)
46529
+ break;
46530
+ }
46531
+ return out;
46532
+ }
46533
+ function promptFrom(answer, idea, p, home) {
46534
+ const prompt = clean(answer?.prompt);
46535
+ if (!prompt)
46536
+ return "";
46537
+ const folder = tilde(p.root, home);
46538
+ const opens = prompt.includes(idea.name) && (prompt.includes(folder) || prompt.includes(p.root));
46539
+ return opens ? prompt : `In ${folder}, make a new Ponzu design called \u201C${idea.name}\u201D. ${prompt}`;
46540
+ }
46541
+ function clean(v) {
46542
+ return typeof v === "string" ? v.replace(/\s+/g, " ").trim() : "";
46543
+ }
46544
+ function stepOf(tool, input, root) {
46545
+ const i = input ?? {};
46546
+ const text = (v) => typeof v === "string" ? v.trim() : "";
46547
+ if (tool === "Read") {
46548
+ const path = text(i.file_path);
46549
+ const rel = path.startsWith(`${root}/`) ? path.slice(root.length + 1) : path;
46550
+ return rel ? { verb: "read", what: rel.split("/").slice(-2).join("/") } : null;
46551
+ }
46552
+ if (tool === "Glob") {
46553
+ const pattern = text(i.pattern);
46554
+ return pattern ? { verb: "find", what: pattern } : null;
46555
+ }
46556
+ if (tool === "Grep") {
46557
+ const words = text(i.pattern).replace(/^\(\?i\)/, "").split("|")[0].replace(/\\[wsdb][+*?]?/g, " ").replace(/\.[+*?]?/g, " ").replace(/[\\^$()[\]{}?*+]/g, "").replace(/\s+/g, " ").trim().slice(0, 40);
46558
+ return words ? { verb: "search", what: words } : null;
46559
+ }
46560
+ return null;
46561
+ }
46562
+ var runAgent = async (job) => {
46563
+ if (job.agent === "claude")
46564
+ return runClaude(job);
46565
+ return runCodex(job);
46566
+ };
46567
+ var ISOLATION = {
46568
+ claude: ["--strict-mcp-config", "--no-session-persistence", "--settings", "--setting-sources"],
46569
+ codex: ["--ephemeral"]
46570
+ };
46571
+ var isolates = async (agent, bin, env) => {
46572
+ const has = flagsOf(await helpOf(bin, agent === "codex" ? ["exec", "--help"] : ["--help"], env));
46573
+ return ISOLATION[agent].every(has);
46574
+ };
46575
+ function flagsOf(help) {
46576
+ return (flag) => new RegExp(`^\\s*(?:-\\w, )?${flag}\\b`, "m").test(help);
46577
+ }
46578
+ async function runClaude(job) {
46579
+ const help = await helpOf(job.bin, ["--help"], job.env);
46580
+ const has = flagsOf(help);
46581
+ if (!ISOLATION.claude.every(has))
46582
+ throw new Error("Claude Code can't run isolated");
46583
+ const tools = job.kind === "glance" && has("--tools") ? "" : "Read,Glob,Grep";
46584
+ const stream = job.kind === "prompt" && has("--include-partial-messages");
46585
+ const args = [
46586
+ "-p",
46587
+ "--output-format",
46588
+ stream ? "stream-json" : "json",
46589
+ ...stream ? ["--verbose", "--include-partial-messages"] : [],
46590
+ ...tools && has("--json-schema") ? ["--json-schema", JSON.stringify(job.schema)] : [],
46591
+ ...has("--tools") ? ["--tools", tools] : [],
46592
+ ...tools ? ["--allowedTools", tools] : [],
46593
+ "--disallowedTools",
46594
+ "Bash,Edit,Write,MultiEdit,NotebookEdit,WebFetch,WebSearch,Task",
46595
+ ...help.includes("dontAsk") ? ["--permission-mode", "dontAsk"] : [],
46596
+ "--setting-sources",
46597
+ "user",
46598
+ "--settings",
46599
+ JSON.stringify({ disableAllHooks: true }),
46600
+ "--strict-mcp-config",
46601
+ "--no-session-persistence",
46602
+ "--model",
46603
+ "sonnet"
46604
+ ];
46605
+ let result = null;
46606
+ const onLine = (line) => {
46607
+ const e = parseJson(line);
46608
+ if (e?.type === "result")
46609
+ result = e;
46610
+ if (e?.type !== "assistant")
46611
+ return;
46612
+ for (const block of e.message?.content ?? []) {
46613
+ if (block?.type !== "tool_use" || !block.name)
46614
+ continue;
46615
+ const step = stepOf(block.name, block.input, job.project.root);
46616
+ if (step)
46617
+ job.onStep?.(step);
46618
+ }
46619
+ };
46620
+ const out = await exec(job.bin, args, {
46621
+ cwd: job.project.root,
46622
+ input: job.prompt,
46623
+ env: job.env,
46624
+ signal: job.signal,
46625
+ ...stream ? { onLine, quiet: job.quiet } : {}
46626
+ });
46627
+ const r = stream ? result : parseJson(out);
46628
+ if (!r || r.is_error)
46629
+ throw new Error("Claude Code didn't answer");
46630
+ return r.structured_output ?? parseJson(r.result ?? "");
46631
+ }
46632
+ async function runCodex(job) {
46633
+ const help = await helpOf(job.bin, ["exec", "--help"], job.env);
46634
+ const has = flagsOf(help);
46635
+ if (!ISOLATION.codex.every(has))
46636
+ throw new Error("Codex can't run isolated");
46637
+ const dir = await mkdtemp(join12(tmpdir2(), "ponzu-suggest-"));
46638
+ try {
46639
+ const work = join12(dir, "work");
46640
+ const schema = join12(dir, "schema.json");
46641
+ const last = join12(dir, "answer.json");
46642
+ await mkdir10(work);
46643
+ await writeFile8(schema, JSON.stringify(job.schema));
46644
+ const off = (feature) => ["-c", `features.${feature}=false`];
46645
+ const args = [
46646
+ "exec",
46647
+ "--sandbox",
46648
+ "read-only",
46649
+ "--skip-git-repo-check",
46650
+ "--color",
46651
+ "never",
46652
+ "-C",
46653
+ work,
46654
+ "--output-schema",
46655
+ schema,
46656
+ "-o",
46657
+ last,
46658
+ "-c",
46659
+ 'approval_policy="never"',
46660
+ ...off("hooks"),
46661
+ ...off("plugins"),
46662
+ ...off("apps"),
46663
+ ...off("memories"),
46664
+ ...has("--ignore-user-config") ? ["--ignore-user-config"] : ["-c", "mcp_servers={}", "-c", "notify=[]"],
46665
+ ...has("--ignore-rules") ? ["--ignore-rules"] : [],
46666
+ "--ephemeral",
46667
+ "-"
46668
+ ];
46669
+ const out = await exec(job.bin, args, {
46670
+ cwd: work,
46671
+ input: job.prompt,
46672
+ env: job.env,
46673
+ signal: job.signal
46674
+ });
46675
+ const answer = await readFile9(last, "utf8").catch(() => out);
46676
+ return parseJson(answer);
46677
+ } finally {
46678
+ await rm4(dir, { recursive: true, force: true });
46679
+ }
46680
+ }
46681
+ function parseJson(text) {
46682
+ try {
46683
+ return JSON.parse(text);
46684
+ } catch {}
46685
+ const start = text.indexOf("{");
46686
+ const end = text.lastIndexOf("}");
46687
+ if (start < 0 || end <= start)
46688
+ return null;
46689
+ try {
46690
+ return JSON.parse(text.slice(start, end + 1));
46691
+ } catch {
46692
+ return null;
46693
+ }
46694
+ }
46695
+ var HELP_FOR = RUN_EVERY;
46696
+ var helps = new Map;
46697
+ function helpOf(bin, args, env) {
46698
+ const key = `${bin} ${args.join(" ")}`;
46699
+ const kept = helps.get(key);
46700
+ if (kept && Date.now() - kept.at < HELP_FOR)
46701
+ return kept.help;
46702
+ const help = exec(bin, args, { env, signal: AbortSignal.timeout(15000) }).catch(() => "");
46703
+ helps.set(key, { at: Date.now(), help });
46704
+ help.then((text) => {
46705
+ if (!text && helps.get(key)?.help === help)
46706
+ helps.delete(key);
46707
+ });
46708
+ return help;
46709
+ }
46710
+ function exec(bin, args, o) {
46711
+ return new Promise((done, fail) => {
46712
+ if (o.signal.aborted)
46713
+ return fail(new Error("stopped"));
46714
+ const child = spawn5(bin, args, {
46715
+ cwd: o.cwd,
46716
+ env: o.env,
46717
+ stdio: ["pipe", "pipe", "pipe"]
46718
+ });
46719
+ let out = "";
46720
+ let err = "";
46721
+ let quiet = false;
46722
+ const stop = () => {
46723
+ child.kill("SIGTERM");
46724
+ setTimeout(() => child.kill("SIGKILL"), 3000).unref();
46725
+ };
46726
+ let hush;
46727
+ const heard = () => {
46728
+ if (!o.quiet)
46729
+ return;
46730
+ clearTimeout(hush);
46731
+ hush = setTimeout(() => {
46732
+ quiet = true;
46733
+ stop();
46734
+ }, o.quiet);
46735
+ };
46736
+ heard();
46737
+ const lines = (flush) => {
46738
+ if (!o.onLine)
46739
+ return;
46740
+ let i = out.indexOf(`
46741
+ `);
46742
+ while (i >= 0) {
46743
+ o.onLine(out.slice(0, i));
46744
+ out = out.slice(i + 1);
46745
+ i = out.indexOf(`
46746
+ `);
46747
+ }
46748
+ if (flush && out)
46749
+ o.onLine(out);
46750
+ };
46751
+ o.signal.addEventListener("abort", stop, { once: true });
46752
+ child.stdout.on("data", (d) => {
46753
+ heard();
46754
+ if (out.length < 4000000)
46755
+ out += d.toString();
46756
+ lines(false);
46757
+ });
46758
+ child.stderr.on("data", (d) => {
46759
+ if (err.length < 4000)
46760
+ err += d.toString();
46761
+ });
46762
+ child.on("error", (e) => {
46763
+ clearTimeout(hush);
46764
+ o.signal.removeEventListener("abort", stop);
46765
+ fail(e);
46766
+ });
46767
+ child.on("close", (code) => {
46768
+ clearTimeout(hush);
46769
+ o.signal.removeEventListener("abort", stop);
46770
+ if (quiet)
46771
+ return fail(new Error("went quiet"));
46772
+ if (o.signal.aborted)
46773
+ return fail(new Error("timed out"));
46774
+ lines(true);
46775
+ if (code === 0)
46776
+ done(o.onLine ? "" : out);
46777
+ else
46778
+ fail(new Error(err.trim().split(`
46779
+ `).pop() || `exited with ${code}`));
46780
+ });
46781
+ child.stdin.on("error", () => {});
46782
+ child.stdin.end(o.input ?? "");
46783
+ });
46784
+ }
46785
+
46027
46786
  // packages/server/src/serve.ts
46028
46787
  var PROTOCOL = 1;
46029
46788
  var PHONE_ACTIVE_MS = 2 * 60000;
@@ -46142,6 +46901,11 @@ function serve(ws, opts = {}) {
46142
46901
  agentWorking
46143
46902
  });
46144
46903
  const feed = new CatalogFeed(home, ws);
46904
+ const suggestions = opts.suggest ? new SuggestionService({
46905
+ ...opts.suggest,
46906
+ agents: opts.agents,
46907
+ designs: async () => (await home.local()).files
46908
+ }) : null;
46145
46909
  const idleMs = () => idleFor({ agentIdle: mcp.idleMs(), phoneAt, phoneSockets: phoneSockets(), now: Date.now() });
46146
46910
  const handle = async (req, srv, caller) => {
46147
46911
  const url = new URL(req.url);
@@ -46244,6 +47008,16 @@ function serve(ws, opts = {}) {
46244
47008
  feed.poke(0);
46245
47009
  return Response.json({ ok: true });
46246
47010
  }
47011
+ if (path === "/api/suggestions" || path === "/api/suggestions/refresh") {
47012
+ if (caller !== "local" || !sameHostOk(req, url))
47013
+ return new Response("Forbidden", { status: 403 });
47014
+ const refresh = path === "/api/suggestions/refresh";
47015
+ if (refresh && req.method !== "POST")
47016
+ return new Response("Method not allowed", { status: 405 });
47017
+ if (!suggestions)
47018
+ return Response.json({ agent: null, ideas: [], running: false, looking: [] });
47019
+ return Response.json(await (refresh ? suggestions.refresh() : suggestions.state()));
47020
+ }
46247
47021
  if (path === "/api/update") {
46248
47022
  if (req.method !== "POST")
46249
47023
  return new Response("Method not allowed", { status: 405 });
@@ -46664,6 +47438,7 @@ function serve(ws, opts = {}) {
46664
47438
  clearInterval(catchUp);
46665
47439
  updater?.stop();
46666
47440
  feed.stop();
47441
+ suggestions?.stop();
46667
47442
  advert?.stop();
46668
47443
  advert = null;
46669
47444
  tailnetServer?.stop(force);
@@ -46808,10 +47583,10 @@ async function serveStudio(path, dir) {
46808
47583
  <p>MCP endpoint: <code>/mcp</code></p></body>`, { headers: { "content-type": "text/html; charset=utf-8" } });
46809
47584
  }
46810
47585
  const clean = normalize(path).replace(/^(\.\.[/\\])+/, "");
46811
- let file = join12(dir, clean);
47586
+ let file = join13(dir, clean);
46812
47587
  if (!file.startsWith(dir) || !existsSync8(file) || statSync2(file).isDirectory())
46813
- file = join12(dir, "index.html");
46814
- const body = await readFile9(file);
47588
+ file = join13(dir, "index.html");
47589
+ const body = await readFile10(file);
46815
47590
  const ext = extname(file);
46816
47591
  return new Response(body, {
46817
47592
  headers: {
@@ -46863,7 +47638,7 @@ function remoteUpdater(endpoint, token) {
46863
47638
  // apps/cli/package.json
46864
47639
  var package_default = {
46865
47640
  name: "ponzu",
46866
- version: "1.68.2",
47641
+ version: "1.69.0",
46867
47642
  description: "The open source canvas your agent draws on in HTML and CSS.",
46868
47643
  type: "module",
46869
47644
  license: "MIT",
@@ -46885,24 +47660,24 @@ var package_default = {
46885
47660
  };
46886
47661
 
46887
47662
  // apps/cli/src/host.ts
46888
- import { spawn as spawn5 } from "child_process";
47663
+ import { spawn as spawn6 } from "child_process";
46889
47664
  import { existsSync as existsSync9, readFileSync as readFileSync2, statSync as statSync3 } from "fs";
46890
- import { chmod, copyFile, mkdir as mkdir10, rename as rename8, rm as rm4, utimes as utimes3, writeFile as writeFile8 } from "fs/promises";
46891
- import { homedir as homedir4 } from "os";
46892
- import { dirname as dirname5, join as join13, posix, win32 } from "path";
46893
- var ponzuHome = () => process.env.PONZU_HOME ?? join13(homedir4(), ".ponzu");
46894
- var hostFile = () => join13(ponzuHome(), "host.json");
47665
+ import { chmod, copyFile, mkdir as mkdir11, rename as rename9, rm as rm5, utimes as utimes3, writeFile as writeFile9 } from "fs/promises";
47666
+ import { homedir as homedir5 } from "os";
47667
+ import { dirname as dirname6, join as join14, posix, win32 } from "path";
47668
+ var ponzuHome = () => process.env.PONZU_HOME ?? join14(homedir5(), ".ponzu");
47669
+ var hostFile = () => join14(ponzuHome(), "host.json");
46895
47670
  async function announceHost(url, opts = {}) {
46896
47671
  if (!opts.takeOver && await runningHost())
46897
47672
  return async () => {};
46898
47673
  const path = hostFile();
46899
- await mkdir10(dirname5(path), { recursive: true });
47674
+ await mkdir11(dirname6(path), { recursive: true });
46900
47675
  const me = { url, mcp: `${url}/mcp`, pid: process.pid };
46901
- await writeFile8(path, `${JSON.stringify(me)}
47676
+ await writeFile9(path, `${JSON.stringify(me)}
46902
47677
  `);
46903
47678
  return async () => {
46904
47679
  if (readHost()?.pid === process.pid)
46905
- await rm4(path, { force: true });
47680
+ await rm5(path, { force: true });
46906
47681
  };
46907
47682
  }
46908
47683
  function readHost() {
@@ -46926,8 +47701,8 @@ async function runningHost() {
46926
47701
  }
46927
47702
  var SERVER = "Contents/Resources/server/ponzu";
46928
47703
  var pathFor = (os) => os === "win32" ? win32 : posix;
46929
- var appImageServer = () => join13(ponzuHome(), "app/ponzu");
46930
- var appImageRecord = () => join13(ponzuHome(), "app/appimage");
47704
+ var appImageServer = () => join14(ponzuHome(), "app/ponzu");
47705
+ var appImageRecord = () => join14(ponzuHome(), "app/appimage");
46931
47706
  function recordedAppImage() {
46932
47707
  try {
46933
47708
  const path = readFileSync2(appImageRecord(), "utf8").trim();
@@ -46945,7 +47720,7 @@ function isFolder(app) {
46945
47720
  }
46946
47721
  function appServer(app, os = process.platform) {
46947
47722
  if (os === "darwin")
46948
- return join13(app, SERVER);
47723
+ return join14(app, SERVER);
46949
47724
  const path = pathFor(os);
46950
47725
  if (os === "linux" && !isFolder(app))
46951
47726
  return appImageServer();
@@ -46978,7 +47753,7 @@ function ownBundle(execPath = process.execPath, os = process.platform, env = pro
46978
47753
  }
46979
47754
  function installedApps(os = process.platform, env = process.env) {
46980
47755
  if (os === "darwin")
46981
- return ["/Applications/Ponzu.app", join13(homedir4(), "Applications/Ponzu.app")];
47756
+ return ["/Applications/Ponzu.app", join14(homedir5(), "Applications/Ponzu.app")];
46982
47757
  if (os === "win32") {
46983
47758
  return [
46984
47759
  env.LOCALAPPDATA && win32.join(env.LOCALAPPDATA, "Programs", "ponzu"),
@@ -47011,15 +47786,15 @@ async function adoptAppImage(execPath = process.execPath, os = process.platform,
47011
47786
  const from = statSync3(execPath);
47012
47787
  const to = statSync3(copy, { throwIfNoEntry: false });
47013
47788
  if (!to || to.size !== from.size || Math.trunc(to.mtimeMs) !== Math.trunc(from.mtimeMs)) {
47014
- await mkdir10(dirname5(copy), { recursive: true });
47789
+ await mkdir11(dirname6(copy), { recursive: true });
47015
47790
  const temp = `${copy}.${process.pid}`;
47016
47791
  await copyFile(execPath, temp);
47017
47792
  await chmod(temp, 493);
47018
47793
  await utimes3(temp, from.atime, from.mtime);
47019
- await rename8(temp, copy);
47794
+ await rename9(temp, copy);
47020
47795
  }
47021
47796
  if (recordedAppImage() !== appimage)
47022
- await writeFile8(appImageRecord(), `${appimage}
47797
+ await writeFile9(appImageRecord(), `${appimage}
47023
47798
  `);
47024
47799
  return appimage;
47025
47800
  }
@@ -47047,7 +47822,7 @@ function startApp(app) {
47047
47822
  const [cmd, args] = launchCommand(app);
47048
47823
  return new Promise((resolve) => {
47049
47824
  try {
47050
- const child = spawn5(cmd, args, { detached: true, stdio: "ignore", env });
47825
+ const child = spawn6(cmd, args, { detached: true, stdio: "ignore", env });
47051
47826
  child.on("error", () => resolve());
47052
47827
  child.on("spawn", () => resolve());
47053
47828
  child.unref();
@@ -47075,7 +47850,7 @@ async function findHost(launch) {
47075
47850
  lastFailed = Date.now();
47076
47851
  return url;
47077
47852
  }
47078
- var shimPath = (os = process.platform) => join13(ponzuHome(), os === "win32" ? "bin/ponzu.cmd" : "bin/ponzu");
47853
+ var shimPath = (os = process.platform) => join14(ponzuHome(), os === "win32" ? "bin/ponzu.cmd" : "bin/ponzu");
47079
47854
  function shimScript(app, os = process.platform) {
47080
47855
  if (os === "win32") {
47081
47856
  const candidates = [
@@ -47120,8 +47895,8 @@ exit 1
47120
47895
  }
47121
47896
  async function writeShim(app) {
47122
47897
  const path = shimPath();
47123
- await mkdir10(dirname5(path), { recursive: true });
47124
- await writeFile8(path, shimScript(app));
47898
+ await mkdir11(dirname6(path), { recursive: true });
47899
+ await writeFile9(path, shimScript(app));
47125
47900
  await chmod(path, 493);
47126
47901
  return path;
47127
47902
  }
@@ -47129,27 +47904,27 @@ async function writeShim(app) {
47129
47904
  // apps/cli/src/install.ts
47130
47905
  import { spawnSync as spawnSync2 } from "child_process";
47131
47906
  import { existsSync as existsSync11, readFileSync as readFileSync4 } from "fs";
47132
- import { mkdir as mkdir12, writeFile as writeFile10 } from "fs/promises";
47133
- import { homedir as homedir6 } from "os";
47134
- import { delimiter as delimiter2, dirname as dirname7, join as join15 } from "path";
47907
+ import { mkdir as mkdir13, writeFile as writeFile11 } from "fs/promises";
47908
+ import { homedir as homedir7 } from "os";
47909
+ import { delimiter as delimiter2, dirname as dirname8, join as join16 } from "path";
47135
47910
 
47136
47911
  // apps/cli/src/command.ts
47137
47912
  import { spawnSync } from "child_process";
47138
47913
  import { accessSync, constants, existsSync as existsSync10, lstatSync, readFileSync as readFileSync3, realpathSync as realpathSync3 } from "fs";
47139
- import { mkdir as mkdir11, symlink, writeFile as writeFile9 } from "fs/promises";
47140
- import { homedir as homedir5 } from "os";
47141
- import { delimiter, dirname as dirname6, join as join14 } from "path";
47142
- function commandDirs(home = homedir5()) {
47143
- return [join14(home, ".local/bin"), join14(home, "bin"), "/usr/local/bin"];
47914
+ import { mkdir as mkdir12, symlink, writeFile as writeFile10 } from "fs/promises";
47915
+ import { homedir as homedir6 } from "os";
47916
+ import { delimiter, dirname as dirname7, join as join15 } from "path";
47917
+ function commandDirs(home = homedir6()) {
47918
+ return [join15(home, ".local/bin"), join15(home, "bin"), "/usr/local/bin"];
47144
47919
  }
47145
- var stateFile = () => join14(ponzuHome(), "command.json");
47920
+ var stateFile = () => join15(ponzuHome(), "command.json");
47146
47921
  async function linkCommand(shim, opts) {
47147
47922
  const say = (s) => {
47148
47923
  if (!opts.auto)
47149
47924
  console.log(s);
47150
47925
  };
47151
47926
  if (process.platform === "win32")
47152
- return addToUserPath(dirname6(shim), opts.auto, say);
47927
+ return addToUserPath(dirname7(shim), opts.auto, say);
47153
47928
  const found = Bun.which("ponzu", { PATH: opts.path.join(delimiter) });
47154
47929
  const app = found && opensApp(found) ? found : null;
47155
47930
  if (found && !app) {
@@ -47160,25 +47935,25 @@ async function linkCommand(shim, opts) {
47160
47935
  const state = readState();
47161
47936
  if (opts.auto && state.linked)
47162
47937
  return;
47163
- const before = app ? opts.path.indexOf(dirname6(app)) : -1;
47938
+ const before = app ? opts.path.indexOf(dirname7(app)) : -1;
47164
47939
  const dir = commandDirs(opts.home).find((d) => {
47165
47940
  const at = opts.path.indexOf(d);
47166
47941
  return at >= 0 && (before < 0 || at < before) && canLink(d, opts.home);
47167
47942
  });
47168
47943
  if (!dir) {
47169
- say(process.platform === "darwin" ? `To run ponzu from a terminal, add ${dirname6(shim)} to your PATH, or choose Install Terminal Command in Ponzu's app menu.` : `To run ponzu from a terminal, add ${dirname6(shim)} to your PATH.`);
47944
+ say(process.platform === "darwin" ? `To run ponzu from a terminal, add ${dirname7(shim)} to your PATH, or choose Install Terminal Command in Ponzu's app menu.` : `To run ponzu from a terminal, add ${dirname7(shim)} to your PATH.`);
47170
47945
  return;
47171
47946
  }
47172
- const link = join14(dir, "ponzu");
47947
+ const link = join15(dir, "ponzu");
47173
47948
  try {
47174
- await mkdir11(dir, { recursive: true });
47949
+ await mkdir12(dir, { recursive: true });
47175
47950
  await symlink(shim, link);
47176
47951
  } catch {
47177
47952
  say(`Couldn't link ponzu into ${dir}.`);
47178
47953
  return;
47179
47954
  }
47180
- await mkdir11(dirname6(stateFile()), { recursive: true });
47181
- await writeFile9(stateFile(), `${JSON.stringify({ linked: link }, null, 2)}
47955
+ await mkdir12(dirname7(stateFile()), { recursive: true });
47956
+ await writeFile10(stateFile(), `${JSON.stringify({ linked: link }, null, 2)}
47182
47957
  `);
47183
47958
  say(`Linked ${link}, so ponzu runs from a terminal.`);
47184
47959
  }
@@ -47216,8 +47991,8 @@ async function addToUserPath(dir, auto, say) {
47216
47991
  return;
47217
47992
  if (userPath(dir, true) !== "added")
47218
47993
  return say(`Couldn't add ${dir} to your PATH. Add it yourself to run ponzu from a terminal.`);
47219
- await mkdir11(dirname6(stateFile()), { recursive: true });
47220
- await writeFile9(stateFile(), `${JSON.stringify({ linked: dir }, null, 2)}
47994
+ await mkdir12(dirname7(stateFile()), { recursive: true });
47995
+ await writeFile10(stateFile(), `${JSON.stringify({ linked: dir }, null, 2)}
47221
47996
  `);
47222
47997
  say(`Added ${dir} to your PATH. Open a new terminal and run ponzu.`);
47223
47998
  }
@@ -47228,9 +48003,9 @@ function readState() {
47228
48003
  return {};
47229
48004
  }
47230
48005
  }
47231
- function canLink(dir, home = homedir5()) {
48006
+ function canLink(dir, home = homedir6()) {
47232
48007
  try {
47233
- lstatSync(join14(dir, "ponzu"));
48008
+ lstatSync(join15(dir, "ponzu"));
47234
48009
  return false;
47235
48010
  } catch {}
47236
48011
  if (!existsSync10(dir))
@@ -47244,7 +48019,7 @@ function canLink(dir, home = homedir5()) {
47244
48019
  }
47245
48020
  function opensApp(file) {
47246
48021
  try {
47247
- return existsSync10(join14(dirname6(realpathSync3(file)), "resources/server/ponzu"));
48022
+ return existsSync10(join15(dirname7(realpathSync3(file)), "resources/server/ponzu"));
47248
48023
  } catch {
47249
48024
  return false;
47250
48025
  }
@@ -47264,7 +48039,7 @@ var AGENTS = [
47264
48039
  label: "Claude Code",
47265
48040
  detect: () => Boolean(which("claude")),
47266
48041
  current: () => {
47267
- const config = readJson(join15(homedir6(), ".claude.json"));
48042
+ const config = readJson(join16(homedir7(), ".claude.json"));
47268
48043
  return toEntry(config?.mcpServers?.ponzu);
47269
48044
  },
47270
48045
  register(entry) {
@@ -47314,7 +48089,7 @@ var AGENTS = [
47314
48089
  {
47315
48090
  id: "cursor",
47316
48091
  label: "Cursor",
47317
- detect: () => existsSync11(join15(homedir6(), ".cursor")),
48092
+ detect: () => existsSync11(join16(homedir7(), ".cursor")),
47318
48093
  current: () => {
47319
48094
  const config = readJson(cursorConfig());
47320
48095
  return toEntry(config?.mcpServers?.ponzu);
@@ -47332,8 +48107,8 @@ var AGENTS = [
47332
48107
  config = parsed;
47333
48108
  }
47334
48109
  config.mcpServers = { ...config.mcpServers, ponzu: entry };
47335
- await mkdir12(dirname7(path), { recursive: true });
47336
- await writeFile10(path, `${JSON.stringify(config, null, 2)}
48110
+ await mkdir13(dirname8(path), { recursive: true });
48111
+ await writeFile11(path, `${JSON.stringify(config, null, 2)}
47337
48112
  `);
47338
48113
  return true;
47339
48114
  }
@@ -47401,7 +48176,7 @@ function agentStatus() {
47401
48176
  }
47402
48177
  function codexConfigured() {
47403
48178
  try {
47404
- return /^\[mcp_servers\.ponzu\]/m.test(readFileSync4(join15(process.env.CODEX_HOME ?? join15(homedir6(), ".codex"), "config.toml"), "utf8"));
48179
+ return /^\[mcp_servers\.ponzu\]/m.test(readFileSync4(join16(process.env.CODEX_HOME ?? join16(homedir7(), ".codex"), "config.toml"), "utf8"));
47405
48180
  } catch {
47406
48181
  return false;
47407
48182
  }
@@ -47445,18 +48220,18 @@ function shouldAutoRegister(current, setUpBefore) {
47445
48220
  function isLegacy(e) {
47446
48221
  return "url" in e && /^http:\/\/(127\.0\.0\.1|localhost):7777\/mcp\/?$/.test(e.url);
47447
48222
  }
47448
- var doneFile = () => join15(ponzuHome(), "agents.json");
48223
+ var doneFile = () => join16(ponzuHome(), "agents.json");
47449
48224
  function readDone() {
47450
48225
  const raw = readJson(doneFile());
47451
48226
  return new Set(raw?.configured ?? []);
47452
48227
  }
47453
48228
  async function writeDone(done) {
47454
- await mkdir12(dirname7(doneFile()), { recursive: true });
47455
- await writeFile10(doneFile(), `${JSON.stringify({ configured: [...done].sort() }, null, 2)}
48229
+ await mkdir13(dirname8(doneFile()), { recursive: true });
48230
+ await writeFile11(doneFile(), `${JSON.stringify({ configured: [...done].sort() }, null, 2)}
47456
48231
  `);
47457
48232
  }
47458
48233
  function cursorConfig() {
47459
- return join15(homedir6(), ".cursor", "mcp.json");
48234
+ return join16(homedir7(), ".cursor", "mcp.json");
47460
48235
  }
47461
48236
  function readJson(path) {
47462
48237
  try {
@@ -47469,13 +48244,13 @@ var searchPath;
47469
48244
  function agentPath() {
47470
48245
  if (searchPath !== undefined)
47471
48246
  return searchPath;
47472
- const home = homedir6();
48247
+ const home = homedir7();
47473
48248
  const dirs = [
47474
48249
  ...(process.env.PATH ?? "").split(delimiter2),
47475
- join15(home, ".local/bin"),
47476
- join15(home, ".claude/local"),
47477
- join15(home, ".bun/bin"),
47478
- join15(home, ".npm-global/bin"),
48250
+ join16(home, ".local/bin"),
48251
+ join16(home, ".claude/local"),
48252
+ join16(home, ".bun/bin"),
48253
+ join16(home, ".npm-global/bin"),
47479
48254
  "/opt/homebrew/bin",
47480
48255
  "/usr/local/bin",
47481
48256
  ...loginPath()
@@ -47503,6 +48278,12 @@ function loginPath() {
47503
48278
  function which(cmd) {
47504
48279
  return Bun.which(cmd, { PATH: agentPath() });
47505
48280
  }
48281
+ function agentCli(cmd) {
48282
+ return which(cmd);
48283
+ }
48284
+ function agentEnv() {
48285
+ return { PATH: agentPath() };
48286
+ }
47506
48287
  function capture2(cmd) {
47507
48288
  const bin = which(cmd[0]);
47508
48289
  if (!bin)
@@ -47529,13 +48310,13 @@ function run(cmd, quiet = false) {
47529
48310
  // apps/cli/src/source.ts
47530
48311
  import { createHash as createHash5 } from "crypto";
47531
48312
  import { existsSync as existsSync12 } from "fs";
47532
- import { dirname as dirname8, join as join16, sep as sep4 } from "path";
48313
+ import { dirname as dirname9, join as join17, sep as sep4 } from "path";
47533
48314
  import { fileURLToPath } from "url";
47534
48315
  function sourceCheckout(file = fileURLToPath(import.meta.url)) {
47535
48316
  if (file.startsWith("/$bunfs/") || file.split(sep4).includes("node_modules"))
47536
48317
  return null;
47537
- for (let dir = dirname8(file);dir !== dirname8(dir); dir = dirname8(dir))
47538
- if (existsSync12(join16(dir, ".git")))
48318
+ for (let dir = dirname9(file);dir !== dirname9(dir); dir = dirname9(dir))
48319
+ if (existsSync12(join17(dir, ".git")))
47539
48320
  return dir;
47540
48321
  return null;
47541
48322
  }
@@ -47574,12 +48355,12 @@ function firstFree(ports, hostname) {
47574
48355
  // apps/cli/src/telemetry.ts
47575
48356
  import { createHash as createHash6, randomUUID } from "crypto";
47576
48357
  import { readFileSync as readFileSync5 } from "fs";
47577
- import { mkdir as mkdir13, rename as rename9, writeFile as writeFile11 } from "fs/promises";
48358
+ import { mkdir as mkdir14, rename as rename10, writeFile as writeFile12 } from "fs/promises";
47578
48359
  import { arch, platform as platform2 } from "os";
47579
- import { dirname as dirname10, join as join17 } from "path";
48360
+ import { dirname as dirname11, join as join18 } from "path";
47580
48361
 
47581
48362
  // node_modules/.bun/posthog-node@5.56.2/node_modules/posthog-node/dist/extensions/error-tracking/modifiers/module.node.mjs
47582
- import { dirname as dirname9, posix as posix2, sep as sep5 } from "path";
48363
+ import { dirname as dirname10, posix as posix2, sep as sep5 } from "path";
47583
48364
  function createModulerModifier() {
47584
48365
  const getModuleFromFileName = createGetModuleFromFilename();
47585
48366
  return async (frames) => {
@@ -47588,7 +48369,7 @@ function createModulerModifier() {
47588
48369
  return frames;
47589
48370
  };
47590
48371
  }
47591
- function createGetModuleFromFilename(basePath = process.argv[1] ? dirname9(process.argv[1]) : process.cwd(), isWindows = sep5 === "\\") {
48372
+ function createGetModuleFromFilename(basePath = process.argv[1] ? dirname10(process.argv[1]) : process.cwd(), isWindows = sep5 === "\\") {
47592
48373
  const normalizedBase = isWindows ? normalizeWindowsPath(basePath) : basePath;
47593
48374
  return (filename) => {
47594
48375
  if (!filename)
@@ -56868,7 +57649,7 @@ var CATEGORY = {
56868
57649
  praise: "Praise",
56869
57650
  other: "Other"
56870
57651
  };
56871
- var settingsFile = () => join17(ponzuHome(), "telemetry.json");
57652
+ var settingsFile = () => join18(ponzuHome(), "telemetry.json");
56872
57653
  function readSettings() {
56873
57654
  try {
56874
57655
  return JSON.parse(readFileSync5(settingsFile(), "utf8"));
@@ -56878,11 +57659,11 @@ function readSettings() {
56878
57659
  }
56879
57660
  async function writeSettings(settings) {
56880
57661
  const path = settingsFile();
56881
- await mkdir13(dirname10(path), { recursive: true });
57662
+ await mkdir14(dirname11(path), { recursive: true });
56882
57663
  const tmp = `${path}.${process.pid}.tmp`;
56883
- await writeFile11(tmp, `${JSON.stringify(settings, null, 2)}
57664
+ await writeFile12(tmp, `${JSON.stringify(settings, null, 2)}
56884
57665
  `);
56885
- await rename9(tmp, path);
57666
+ await rename10(tmp, path);
56886
57667
  }
56887
57668
  async function settings() {
56888
57669
  const current = readSettings();
@@ -57055,13 +57836,13 @@ async function telemetryCommand(arg = "status") {
57055
57836
  import { dlopen, FFIType, ptr } from "bun:ffi";
57056
57837
  import { spawnSync as spawnSync3 } from "child_process";
57057
57838
  import { fstatSync, readFileSync as readFileSync6 } from "fs";
57058
- import { mkdir as mkdir14, rm as rm5, writeFile as writeFile12 } from "fs/promises";
57059
- import { homedir as homedir7 } from "os";
57060
- import { join as join18 } from "path";
57839
+ import { mkdir as mkdir15, rm as rm6, writeFile as writeFile13 } from "fs/promises";
57840
+ import { homedir as homedir8 } from "os";
57841
+ import { join as join19 } from "path";
57061
57842
  var WAKE_LABEL = "design.ponzu.app.wake";
57062
57843
  var WAKE_PORT = 7778;
57063
- var agentsDir = () => process.env.PONZU_LAUNCH_AGENTS ?? join18(homedir7(), "Library/LaunchAgents");
57064
- var wakePlistPath = () => join18(agentsDir(), `${WAKE_LABEL}.plist`);
57844
+ var agentsDir = () => process.env.PONZU_LAUNCH_AGENTS ?? join19(homedir8(), "Library/LaunchAgents");
57845
+ var wakePlistPath = () => join19(agentsDir(), `${WAKE_LABEL}.plist`);
57065
57846
  var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
57066
57847
  function wakePlist(program, port = WAKE_PORT) {
57067
57848
  return `<?xml version="1.0" encoding="UTF-8"?>
@@ -57107,17 +57888,17 @@ async function installWake(program, run = launchctl) {
57107
57888
  } catch {}
57108
57889
  if (current === plist && run(["print", `${domain()}/${WAKE_LABEL}`]))
57109
57890
  return false;
57110
- await mkdir14(agentsDir(), { recursive: true });
57111
- await writeFile12(path, plist);
57891
+ await mkdir15(agentsDir(), { recursive: true });
57892
+ await writeFile13(path, plist);
57112
57893
  run(["bootout", `${domain()}/${WAKE_LABEL}`]);
57113
57894
  run(["bootstrap", domain(), path]);
57114
57895
  return true;
57115
57896
  }
57116
57897
  async function uninstallWake(run = launchctl) {
57117
57898
  run(["bootout", `${domain()}/${WAKE_LABEL}`]);
57118
- await rm5(wakePlistPath(), { force: true });
57899
+ await rm6(wakePlistPath(), { force: true });
57119
57900
  }
57120
- var systemdDir = () => process.env.PONZU_SYSTEMD_USER ?? join18(process.env.XDG_CONFIG_HOME || join18(homedir7(), ".config"), "systemd/user");
57901
+ var systemdDir = () => process.env.PONZU_SYSTEMD_USER ?? join19(process.env.XDG_CONFIG_HOME || join19(homedir8(), ".config"), "systemd/user");
57121
57902
  var WAKE_SOCKET = "ponzu-wake.socket";
57122
57903
  var WAKE_SERVICE = "ponzu-wake@.service";
57123
57904
  var systemdWord = (s) => `"${s.replace(/[\\"]/g, "\\$&").replace(/%/g, "%%").replace(/\$/g, "$$$$")}"`;
@@ -57151,16 +57932,16 @@ async function installSystemdWake(program, run = systemctl) {
57151
57932
  const units = wakeUnits(program);
57152
57933
  const current = Object.entries(units).every(([name, text]) => {
57153
57934
  try {
57154
- return readFileSync6(join18(systemdDir(), name), "utf8") === text;
57935
+ return readFileSync6(join19(systemdDir(), name), "utf8") === text;
57155
57936
  } catch {
57156
57937
  return false;
57157
57938
  }
57158
57939
  });
57159
57940
  if (current && run(["is-active", "--quiet", WAKE_SOCKET]))
57160
57941
  return false;
57161
- await mkdir14(systemdDir(), { recursive: true });
57942
+ await mkdir15(systemdDir(), { recursive: true });
57162
57943
  for (const [name, text] of Object.entries(units))
57163
- await writeFile12(join18(systemdDir(), name), text);
57944
+ await writeFile13(join19(systemdDir(), name), text);
57164
57945
  run(["daemon-reload"]);
57165
57946
  run(["enable", WAKE_SOCKET]);
57166
57947
  run(["restart", WAKE_SOCKET]);
@@ -57169,7 +57950,7 @@ async function installSystemdWake(program, run = systemctl) {
57169
57950
  async function uninstallSystemdWake(run = systemctl) {
57170
57951
  run(["disable", "--now", WAKE_SOCKET]);
57171
57952
  for (const name of [WAKE_SOCKET, WAKE_SERVICE])
57172
- await rm5(join18(systemdDir(), name), { force: true });
57953
+ await rm6(join19(systemdDir(), name), { force: true });
57173
57954
  run(["daemon-reload"]);
57174
57955
  }
57175
57956
  function peerKind(ip) {
@@ -57334,7 +58115,7 @@ function lsofPeer() {
57334
58115
 
57335
58116
  // apps/cli/src/index.ts
57336
58117
  var VERSION2 = package_default.version;
57337
- var here = dirname11(fileURLToPath2(import.meta.url));
58118
+ var here = dirname12(fileURLToPath2(import.meta.url));
57338
58119
  var HELP = `Ponzu ${VERSION2}: the open source canvas your agent draws on in HTML and CSS.
57339
58120
 
57340
58121
  Usage
@@ -57459,7 +58240,7 @@ async function openWorkspace(files, log) {
57459
58240
  const paths = files.length ? files.map((f) => resolve5(f)) : await findDesigns(process.cwd());
57460
58241
  const named = [];
57461
58242
  for (const p of paths) {
57462
- if (!existsSync13(join19(p, "design.json")) && existsSync13(p) === false && files.length) {
58243
+ if (!existsSync13(join20(p, "design.json")) && existsSync13(p) === false && files.length) {
57463
58244
  log(`Creating ${p}`);
57464
58245
  }
57465
58246
  const { key } = await ws.open(p);
@@ -57485,10 +58266,15 @@ async function runServe(files, o) {
57485
58266
  root: process.cwd(),
57486
58267
  tailnet: o.tailnet,
57487
58268
  tailnetPort: 7777,
57488
- dbFile: join19(ponzuHome(), "ponzu.db"),
57489
- stateFile: join19(ponzuHome(), "catalog.json"),
58269
+ dbFile: join20(ponzuHome(), "ponzu.db"),
58270
+ stateFile: join20(ponzuHome(), "catalog.json"),
57490
58271
  spotlight: process.env.PONZU_SPOTLIGHT === "0" ? false : undefined,
57491
58272
  agents: async () => agents(),
58273
+ suggest: {
58274
+ bin: (agent) => agentCli(agent),
58275
+ env: agentEnv(),
58276
+ file: join20(ponzuHome(), "suggestions.json")
58277
+ },
57492
58278
  opened: named,
57493
58279
  version: VERSION2,
57494
58280
  telemetry: phoneTelemetry(),
@@ -57594,7 +58380,7 @@ function absolutePaths(args, cwd) {
57594
58380
  }
57595
58381
  async function runInit(target = "design/untitled.design") {
57596
58382
  const path = resolve5(target.endsWith(".design") ? target : `${target}.design`);
57597
- if (existsSync13(join19(path, "design.json"))) {
58383
+ if (existsSync13(join20(path, "design.json"))) {
57598
58384
  console.error(`${path} already exists.`);
57599
58385
  process.exit(1);
57600
58386
  }
@@ -57616,11 +58402,11 @@ Next: ponzu serve ${target}`);
57616
58402
  }
57617
58403
  function findStudio() {
57618
58404
  const candidates = [
57619
- join19(here, "../studio"),
57620
- join19(here, "../../studio/dist"),
57621
- join19(here, "../../../apps/studio/dist")
58405
+ join20(here, "../studio"),
58406
+ join20(here, "../../studio/dist"),
58407
+ join20(here, "../../../apps/studio/dist")
57622
58408
  ];
57623
- return candidates.find((c) => existsSync13(join19(c, "index.html")));
58409
+ return candidates.find((c) => existsSync13(join20(c, "index.html")));
57624
58410
  }
57625
58411
  function lanAddresses() {
57626
58412
  const out = [];