@augurworks/augur 0.15.7 → 0.15.9

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/README.md CHANGED
@@ -53,6 +53,27 @@ contracts in [agents/](./agents/) does the same job. Screens in the demo space
53
53
  carry the session that made them: [open the repo and read one](https://github.com/andratwiro/augur-space-fulla/tree/main/garden/prompts).
54
54
  The follow-ups read like design direction, because that is what they are.
55
55
 
56
+ ## Let an assistant in from a link
57
+
58
+ Someone on the team gives their assistant the workspace address and asks for a
59
+ change. Nothing else is needed from them, and no password ever changes hands.
60
+ The workspace tells the assistant how to get in (`/llms.txt` on every instance),
61
+ and the flow is device pairing:
62
+
63
+ ```bash
64
+ npx @augurworks/augur connect --origin https://<the workspace>
65
+ ```
66
+
67
+ It prints a link and a code. The person opens the link in the browser they are
68
+ already signed in to and types the code; that pairs the one terminal that printed
69
+ it, to publish as them. From then on the assistant opens a prototype as a draft
70
+ (`augur open <opportunity>/<prototype>`), edits it, live at the draft's own
71
+ address, and lands it (`augur land`), which moves the real URL. Signed-in members
72
+ see the same instructions under Help › Building and at `/__connect`, so a person
73
+ who knows nothing technical can still confirm what their assistant was told. The
74
+ package on npm is this repository, and nothing else: `npm view @augurworks/augur
75
+ repository`.
76
+
56
77
  ## Boards where the prototypes run
57
78
 
58
79
  Drop a live prototype next to the stickies and drive it. Everyone on the board
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@augurworks/augur",
3
- "version": "0.15.7",
3
+ "version": "0.15.9",
4
4
  "files": [
5
5
  "scripts/",
6
6
  "build.js",
@@ -19,6 +19,7 @@ import path from "node:path";
19
19
  import os from "node:os";
20
20
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
21
21
  import { resolveOrigin } from "./lib/store.mjs";
22
+ import { augurOnPath } from "./lib/adapters.mjs";
22
23
 
23
24
  const C = { dim: "\x1b[2m", bold: "\x1b[1m", ok: "\x1b[32m", warn: "\x1b[33m", off: "\x1b[0m" };
24
25
  const log = (m) => console.log(`\x1b[35m[connect]\x1b[0m ${m}`);
@@ -64,6 +65,37 @@ async function post(pathPart, body) {
64
65
  return { status: r.status, json };
65
66
  }
66
67
 
68
+ // A machine that is ALREADY connected to this origin does not pair again by accident. One
69
+ // cold agent collected its token with a blocking `connect`, did not read the answer, ran
70
+ // `connect` again, minted a second code, and had the person approve that too. If the saved
71
+ // token still answers, say so and stop; `--again` pairs afresh on purpose, and a token the
72
+ // workspace no longer honours (revoked, another workspace's) falls through to a new pairing.
73
+ const AGAIN = argv.includes("--again");
74
+ // Whether this workspace serves drafts, from its own door — the success line names the
75
+ // right next verb, not `publish` on a workspace that refuses it. Asked before any path
76
+ // that can finish, including the one that collects a pairing started earlier.
77
+ let draftsHere = false;
78
+ try { const j = await (await fetch(`${ORIGIN}/.well-known/augur.json`, { headers: { Accept: "application/json" } })).json(); draftsHere = !!(j && j.drafts && j.drafts.enabled); } catch (e) { draftsHere = false; }
79
+ const TOKENS_FILE = path.join(os.homedir(), ".config", "augur", "tokens.json");
80
+ function savedToken() {
81
+ try { const t = JSON.parse(readFileSync(TOKENS_FILE, "utf8"))[host]; return t && t.token ? t : null; } catch (e) { return null; }
82
+ }
83
+ if (!AGAIN && !process.env.AUGUR_TOKEN) {
84
+ const saved = savedToken();
85
+ if (saved && !(saved.expiresAt && Date.parse(saved.expiresAt) <= Date.now())) {
86
+ let live = false;
87
+ try {
88
+ const r = await fetch(`${ORIGIN}/__unit/drafts`, { headers: { Authorization: `Bearer ${saved.token}`, Accept: "application/json" } });
89
+ live = r.status === 200;
90
+ } catch (e) { live = false; }
91
+ if (live) {
92
+ log(`this machine is already connected to ${C.bold}${host}${C.off}${saved.at ? ` (since ${saved.at.slice(0, 16).replace("T", " ")})` : ""}. Nothing to approve.`);
93
+ console.log(` ${C.dim}\`augur open <opportunity>/<prototype>\` works from here. \`augur connect --again\` pairs afresh.${C.off}`);
94
+ process.exit(0);
95
+ }
96
+ }
97
+ }
98
+
67
99
  // A pairing this machine already started and nobody has collected: ask once whether it
68
100
  // was approved meanwhile, and if not, keep waiting on THAT code rather than minting a
69
101
  // second one for the same person to type.
@@ -152,7 +184,17 @@ all[new URL(ORIGIN).host] = {
152
184
  writeFileSync(file, JSON.stringify(all, null, 2), { mode: 0o600 });
153
185
 
154
186
  log(`${C.ok}paired — publish access: ${saved.space === "*" ? "all spaces" : saved.space}${C.off}`);
155
- console.log(`ready \`augur publish\` will now use this token for ${ORIGIN}`);
187
+ // The next verb, spelled so it runs from THIS machine: `augur` after a global install, the
188
+ // package's own `npx` line otherwise — and the verb this workspace takes, which the door
189
+ // knows (drafts: open and land; a whole-tree instance: publish).
190
+ const verb = augurOnPath() ? "augur" : "npx @augurworks/augur";
191
+ if (draftsHere) {
192
+ console.log(`ready — \`${verb} open <opportunity>/<prototype>\` opens one prototype as a draft (live at once at its own address);`);
193
+ console.log(` edit the folder it makes, then \`${verb} land\` in that folder: the real URL moves.`);
194
+ } else {
195
+ console.log(`ready — \`${verb} publish\` will now use this token for ${ORIGIN}`);
196
+ }
197
+ if (verb !== "augur") console.log(`${C.dim}(\`augur\` is not on this machine's PATH; \`npm i -g @augurworks/augur\` puts it there for good.)${C.off}`);
156
198
  if (saved.expiresAt) {
157
199
  const days = Math.max(0, Math.round((Date.parse(saved.expiresAt) - Date.now()) / 86400000));
158
200
  console.log(`${C.dim}It expires in ${days} days (${saved.expiresAt.slice(0, 10)}). Run \`augur connect\` again then.${C.off}`);
@@ -107,7 +107,12 @@ export const hookCommand = (event, { onPath = false } = {}) =>
107
107
  export function augurOnPath() {
108
108
  try {
109
109
  const out = execFileSync(process.platform === "win32" ? "where" : "which", ["augur"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
110
- return out.trim().length > 0;
110
+ const found = out.trim().split(/\r?\n/)[0] || "";
111
+ // Inside an `npx` run the package's own bin dir is on PATH and `augur` resolves to a shim
112
+ // in the npx cache — gone the moment npx exits. A hook written as `augur hook pre` from
113
+ // there fails on every later edit (one cold agent's drafts never auto-saved, and its tool
114
+ // showed a "not found" on each write). That resolution counts as NOT on PATH.
115
+ return found.length > 0 && !/[\\/]_npx[\\/]/.test(found);
111
116
  } catch (e) { return false; }
112
117
  }
113
118
  const ours = (h) => !!(h && Array.isArray(h.hooks) && h.hooks.some((x) => x && typeof x.command === "string" && OURS_RE.test(x.command.trim())));
package/scripts/open.mjs CHANGED
@@ -9,7 +9,7 @@ import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import { target, buildStamp } from "./lib/store.mjs";
11
11
  import { unitClient, doOpen, unitPathFor } from "./lib/draft.mjs";
12
- import { installAdapters } from "./lib/adapters.mjs";
12
+ import { installAdapters, augurOnPath } from "./lib/adapters.mjs";
13
13
  import { normUnit } from "../src/unit-core.mjs";
14
14
 
15
15
  const log = (m) => console.error(`\x1b[35m[open]\x1b[0m ${m}`);
@@ -51,7 +51,7 @@ log(r.isNew ? `draft ${r.draftId} on ${unit} — a NEW prototype; ${dir} is empt
51
51
  // here (idempotent; `AUGUR_NO_ADAPTERS=1` skips it — the suite and CI set it).
52
52
  if (!process.env.AUGUR_NO_ADAPTERS) {
53
53
  for (const a of installAdapters()) {
54
- if (a.result === "installed") log(`${a.name}: editor hooks installed (${a.path})edits in a draft folder save on their own; edits to a shared checkout's prototypes are refused.`);
54
+ if (a.result === "installed") log(`${a.name}: a save hook is now in ${a.path} — after each edit inside a draft folder it runs \`augur hook post\`, which saves that draft to ${origin}; before an edit it refuses writes into a shared checkout's prototypes. It does nothing outside draft folders and talks to nothing else; \`augur hook remove\` takes it out.`);
55
55
  else if (a.result === "updated") log(`${a.name}: editor hooks updated (${a.path}).`);
56
56
  }
57
57
  }
@@ -60,4 +60,10 @@ if (r.others.length) {
60
60
  for (const o of r.others) log(` ${o.session || "someone"} (${o.active ? "active" : "idle"})`);
61
61
  log("nothing here stops you; if you both land, the second one syncs first.");
62
62
  }
63
+ // The next two steps, spelled so they run from this machine (`augur` only after a global
64
+ // install; the package's `npx` line otherwise).
65
+ const verb = augurOnPath() ? "augur" : "npx @augurworks/augur";
66
+ log(r.isNew
67
+ ? `next: write ${path.join(dir, "index.html")} (self-contained static HTML); it is live at the address below as soon as it saves. When it is ready: \`cd ${path.basename(dir)} && ${verb} land\` — the last line printed is the live URL.`
68
+ : `next: edit the files in ${dir}; every save is live at the address below. When it is ready: \`cd ${path.basename(dir)} && ${verb} land\` — the last line printed is the live URL.`);
63
69
  console.log(r.address);
package/src/_worker.js CHANGED
@@ -3748,16 +3748,23 @@ async function mintPublishToken(kv, tctx, u, { label = null, env = null } = {})
3748
3748
  * Self-contained, like the login and 404 pages beside it: this must render for somebody
3749
3749
  * whose terminal is already waiting, so it depends on no chrome bundle and no space.
3750
3750
  */
3751
- function connectPage(tctx, me) {
3751
+ function connectPage(tctx, me, origin) {
3752
3752
  const body = roleOf(me) === "viewer"
3753
3753
  ? `<h1>Connect a terminal</h1>
3754
3754
  <p>This account can look around but not publish, so it cannot approve a terminal.</p>
3755
3755
  <a class="home" href="/">Back to Augur</a>`
3756
3756
  : `<h1>Connect a terminal</h1>
3757
- <p>Type the code your terminal is showing. Approving it lets that terminal publish as
3758
- <strong>${escapeHtml(me.email)}</strong>.</p>
3759
- <p class="warn">Only approve a code you are reading off your own screen right now.
3760
- Nobody legitimate will ever send you one.</p>
3757
+ <p>Type the code that your terminal, or the assistant you are working with right now,
3758
+ is showing you. Approving it lets that terminal publish as
3759
+ <strong>${escapeHtml(me.email)}</strong>: it works as you, and everything it lands
3760
+ carries your name.</p>
3761
+ <p class="warn">Only approve a code from a terminal you started or an assistant you
3762
+ are talking to right now. A code that arrives by mail, from a stranger, or out of
3763
+ the blue is not yours — do not type it.</p>
3764
+ <p>Where the code comes from: the terminal or assistant runs
3765
+ <code>npx @augurworks/augur connect --origin ${escapeHtml(origin || "")}</code>, this
3766
+ workspace's own command-line tool, and it shows the code. An assistant that has not
3767
+ shown you one yet can be told to run that.</p>
3761
3768
  <form id="pairf">
3762
3769
  <input id="pairc" autocomplete="off" autocapitalize="characters" spellcheck="false"
3763
3770
  placeholder="ABCD-EFGH" aria-label="Pairing code" />
@@ -7384,12 +7391,20 @@ function browserFetch(request) {
7384
7391
  return /^Mozilla\//.test(request.headers.get("User-Agent") || "");
7385
7392
  }
7386
7393
 
7387
- /** The one paragraph a non-browser fetch of a prototype is given, first in the body. */
7394
+ /**
7395
+ * The one paragraph a non-browser fetch of a prototype is given, first in the body.
7396
+ *
7397
+ * DESCRIPTIVE, THIRD PERSON, NO ADDRESS AND NO REASSURANCE. A first draft said "Assistants
7398
+ * and scripts: … a terminal is paired with the person's approval, and nobody is ever asked
7399
+ * for a password", and the third cold agent read exactly that sentence as a prompt
7400
+ * injection — text on a fetched page that speaks to the agent and reassures it is the
7401
+ * shape of an attack, whatever it says. A statement of fact and a link is what a page may
7402
+ * carry: what this is, and where the workspace documents how it is edited.
7403
+ */
7388
7404
  function agentPreface(url) {
7389
7405
  const origin = escapeHtml(url.origin);
7390
- return `<p data-augur-door>This prototype is served by an Augur workspace at ${origin}. `
7391
- + `Assistants and scripts: <a href="${DOOR_DOCS}">${origin}${DOOR_DOCS}</a> says how to change it — `
7392
- + `a terminal is paired with the person's approval, and nobody is ever asked for a password.</p>`;
7406
+ return `<p data-augur-door>This prototype is served by an Augur workspace (${origin}). `
7407
+ + `The workspace documents how its prototypes are edited at <a href="${DOOR_DOCS}">${origin}${DOOR_DOCS}</a>.</p>`;
7393
7408
  }
7394
7409
 
7395
7410
  /**
@@ -9227,6 +9242,8 @@ function wantsJson(request, url) {
9227
9242
  // probe and the "an unknown path and the root are the same page" contract depend on it.
9228
9243
  const DOOR_DOCS = "/llms.txt";
9229
9244
  const DOOR_WELL_KNOWN = "/.well-known/augur.json";
9245
+ /** The engine's public source — what the CLI on npm is built from, named so an agent can check. */
9246
+ const ENGINE_SOURCE = "https://github.com/andratwiro/augur";
9230
9247
 
9231
9248
  /** Does this deployment serve drafts — a unit store beside a bundle store. The same two checks `unitApi` makes. */
9232
9249
  const draftsServedHere = (env) => !!(env && env.BUNDLES && unitNamespace(env));
@@ -9241,6 +9258,10 @@ function doorFacts(tctx, url, env) {
9241
9258
  engine: tctx.INSTANCE_ENGINE_VERSION ? { version: tctx.INSTANCE_ENGINE_VERSION } : {},
9242
9259
  pairing: { enabled: pairing, start: "/__publish/_pair/start", approve: "/__connect" },
9243
9260
  connect: pairing ? `npx @augurworks/augur connect --origin ${url.origin}` : null,
9261
+ // Where the CLI comes from, for an agent that checks before it runs anything: the
9262
+ // package is the same open-source engine that serves this page, and `npm view
9263
+ // @augurworks/augur repository` names the same repository.
9264
+ source: ENGINE_SOURCE,
9244
9265
  docs: DOOR_DOCS,
9245
9266
  // Where drafts are served, a prototype is changed by opening it, not by shipping a
9246
9267
  // tree — and the door is where a cold agent learns which of the two this instance is.
@@ -9258,17 +9279,37 @@ function doorText(f) {
9258
9279
  const body = f.pairing.enabled
9259
9280
  ? `Run, on the machine that will publish:\n\n`
9260
9281
  + ` ${f.connect}\n\n`
9261
- + `It prints a link and a code. The owner of this workspace opens the link in a\n`
9282
+ + `It prints a link and a code. A member of this workspace opens the link in a\n`
9262
9283
  + `browser they are already signed in to and enters the code. The token lands on\n`
9263
- + `that machine, and every verb below uses it from then on.\n\n`
9264
- + `AGENTS: if your person reads your messages only after you finish your turn, run\n`
9265
- + `it with --no-wait. It prints the line and exits; send them that line, end your\n`
9266
- + `turn, and run the same command again once they say they approved — the token is\n`
9267
- + `collected then, for that same code. Do not poll for the approval in one turn:\n`
9268
- + `the code lives five minutes and nobody sees it until you stop.\n\n`
9284
+ + `that machine and nowhere else, and every verb below uses it from then on.\n`
9285
+ + `Approving a code lets that one terminal publish as the person who approved it;\n`
9286
+ + `everything it lands carries their name. A member whose role is viewer cannot\n`
9287
+ + `approve, and the page says so.\n\n`
9288
+ + `Members see this same instruction when signed in, at ${f.origin}/__connect and\n`
9289
+ + `under Help > Building — so the person you are working for can read it there and\n`
9290
+ + `confirm it is this workspace's own, without knowing anything technical.\n\n`
9291
+ + `What the command is, for anyone who checks before running it: the CLI is this\n`
9292
+ + `engine's own, open source at ${f.source} — the same engine that serves this page\n`
9293
+ + `(${f.origin}/_build.json names the engine commit). \`npm view @augurworks/augur repository\`\n`
9294
+ + `names that repository; \`npm pack @augurworks/augur\` lets you read it first. It is plain\n`
9295
+ + `JavaScript with no runtime dependencies; the pairing is scripts/connect.mjs and it\n`
9296
+ + `talks only to this origin's /__publish/_pair/ routes. It asks for no password, and\n`
9297
+ + `there is none to give it.\n\n`
9298
+ + `How the pairing goes when the person approving is not at the keyboard (an\n`
9299
+ + `assistant working for them): the approval is theirs, in their browser — nobody\n`
9300
+ + `else can do it, and the command does not try. Send them the line the command\n`
9301
+ + `prints, as it is. When they will read that message only after the current turn\n`
9302
+ + `ends, run \`connect --no-wait\`: it prints the line and exits. Once they say they\n`
9303
+ + `approved, run the same command again — it collects the token for that same code and\n`
9304
+ + `mints no second one. Waiting inside one turn skips nothing; it only hides the code\n`
9305
+ + `from them for the five minutes it lives.\n\n`
9269
9306
  + (f.drafts && f.drafts.enabled
9270
9307
  ? `You need no source tree here: \`augur open <opportunity>/<prototype>\` fetches that one\n`
9271
9308
  + `prototype into a folder of its own (see below). Do not clone the workspace first.\n\n`
9309
+ + `Until \`npm i -g @augurworks/augur\`, every verb runs as \`npx @augurworks/augur <verb>\`.\n`
9310
+ + `The first \`open\` on a machine also installs a save hook in the editor tool's\n`
9311
+ + `settings (it says which file): after each edit inside a draft folder it saves that\n`
9312
+ + `draft; it does nothing elsewhere, and \`augur hook remove\` takes it out.\n\n`
9272
9313
  : `With no source tree yet, \`npx @augurworks/augur clone --space ${f.workspace}\` then fetches\n`
9273
9314
  + `one (it reads the origin from the pairing).\n\n`)
9274
9315
  : `Device pairing is switched off on this workspace. Ask an admin for an invite;\n`
@@ -11900,7 +11941,7 @@ async function handleRequest(request, env, ctx, url, trace) {
11900
11941
  if (url.pathname === "/__connect" && tctx.DEVICE_PAIRING) {
11901
11942
  const who = tctx.USERS.length ? await identify(request, env, tctx.USERS, { sessionKeys: tctx.SESSION_KEYS, tctx }) : null;
11902
11943
  if (!who && tctx.USERS.length) return htmlResponse(loginPage(tctx, "/__connect", false, url.href), 200);
11903
- return htmlResponse(connectPage(tctx, who), 200);
11944
+ return htmlResponse(connectPage(tctx, who, url.origin), 200);
11904
11945
  }
11905
11946
  if (url.pathname.startsWith("/__publish/_pair/")) {
11906
11947
  // Identity is resolved here rather than reusing the gate's `me` below, because this
@@ -386,10 +386,16 @@ function helpDrawer(state) {
386
386
  <li>The design system is read-only from a prototype. Edit the skill's own source rather than copying its classes out.</li>
387
387
  </ul>
388
388
 
389
+ <h4>Working with an assistant</h4>
390
+ <ul>
391
+ <li>Give it this workspace's address and say what you want changed or made. It reads how to get in from this site and hands you a link and a short code.</li>
392
+ <li>Open the link here (<code>/__connect</code>) and type the code. That pairs its terminal to publish as you; everything it lands carries your name.</li>
393
+ <li>From then on it opens a prototype as a draft, edits it live at the draft's own address, and lands it when you say so. Viewers cannot approve a code.</li>
394
+ </ul>
395
+
389
396
  <h4>Comment loop <span class="gvhelp__tag">maintainer</span></h4>
390
397
  <ul>
391
- <li><code>npm run review --open</code> lists open threads.</li>
392
- <li>The agent fixes, replies, resolves in-thread. Put it on <code>/loop</code> to keep watching.</li>
398
+ <li>In a checkout, <code>npm run review --open</code> lists open threads; an agent fixes, replies and resolves in-thread.</li>
393
399
  <li>Not automated. You steer it.</li>
394
400
  </ul>${spaceHelpSections(state)}
395
401
  </section>