@figs-so/cli 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/figs.mjs +55 -28
  2. package/package.json +1 -1
package/figs.mjs CHANGED
@@ -31,7 +31,7 @@ import { homedir } from "node:os"
31
31
  import { join } from "node:path"
32
32
  import { randomUUID } from "node:crypto"
33
33
 
34
- const VERSION = "0.1.3"
34
+ const VERSION = "0.1.5"
35
35
  // Going-forward default; override with FIGS_ENDPOINT or .figs/config.json endpoint
36
36
  // (e.g. FIGS_ENDPOINT=http://localhost:3000 for local dev).
37
37
  const DEFAULT_ENDPOINT = "https://app.figs.so"
@@ -197,6 +197,10 @@ async function login(token) {
197
197
  }
198
198
  if (status === "denied") die("authorization denied")
199
199
  if (status === "expired") die("code expired — run `figs login` again")
200
+ if (status === "already_claimed") {
201
+ die("this login was already completed (on another run) — run `figs status` to check; re-run `figs login` if no token was saved")
202
+ }
203
+ if (status === "not_found") die("login code not found — run `figs login` again")
200
204
  // pending → keep polling
201
205
  }
202
206
  die("timed out waiting for approval — run `figs login` again")
@@ -207,6 +211,7 @@ async function status() {
207
211
  const token = getToken()
208
212
  const cfg = readJson(join(repoDir, "config.json"), null)
209
213
  const hasAgent = existsSync(join(repoDir, "agent.json"))
214
+ const hasContract = existsSync(join(repoDir, "CONTRACT.md"))
210
215
  const endpoint = resolveEndpoint()
211
216
 
212
217
  let loggedIn = false
@@ -229,6 +234,7 @@ async function status() {
229
234
  workspaces: list?.map((w) => ({ id: w.id, name: w.name, role: w.role })),
230
235
  config: cfg ? { workspaceId: cfg.workspaceId, agentId: cfg.agentId } : null,
231
236
  agentJson: hasAgent,
237
+ contractMd: hasContract,
232
238
  },
233
239
  null,
234
240
  2,
@@ -255,7 +261,13 @@ async function status() {
255
261
  ? cfg.workspaceId
256
262
  : "not initialized — run `figs init --workspace <id>`",
257
263
  )
258
- row("agent.json", hasAgent ? "present" : "missing — author .figs/agent.json")
264
+ row("agent.json", hasAgent ? "present (identity)" : "missing — author .figs/agent.json")
265
+ row(
266
+ "contract",
267
+ hasContract
268
+ ? "present (activity) — follow it"
269
+ : "none yet — Activity is optional, agree it with your user",
270
+ )
259
271
  row("endpoint", endpoint)
260
272
  row("cli", VERSION)
261
273
  }
@@ -286,31 +298,30 @@ function isUuid(s) {
286
298
  }
287
299
 
288
300
  /**
289
- * The local `.figs/GUIDE.md` is a thin, endpoint-relative pointer the full,
290
- * always-current guide + `agent.json` schema is served at `${endpoint}/llms.txt`
291
- * (one source, no frozen copy to drift). Just enough here to act.
301
+ * The local `.figs/GUIDE.md` is a thin orientation breadcrumb (safe to
302
+ * regenerate) — the full, always-current guide + schema is served at
303
+ * `${endpoint}/llms.txt`, and the agent's own standing agreement lives in the
304
+ * precious, agent-authored `.figs/CONTRACT.md` (which `init` never touches).
292
305
  */
293
306
  function guideStub(endpoint) {
294
- return `<!-- Written by \`figs init\`. Full, always-current guide + schema: ${endpoint}/llms.txt -->
295
- # Figs quickstart for this agent
296
-
297
- You are an AI employee. **Figs** is the read-only window your manager uses to see what you're
298
- responsible for, what you've done, and what you need from them. You publish to it; it never
299
- writes back into this repo.
307
+ return `<!-- Written by \`figs init\` (safe to regenerate). Orientation only the full
308
+ guide lives at the URL below; your own contract lives in CONTRACT.md. -->
309
+ # Figs — this folder
300
310
 
301
- **Full guide + \`agent.json\` schema ${endpoint}/llms.txt** read this before authoring your charter.
311
+ This repo publishes to **Figs** (your manager's read-only window into what you do).
302
312
 
303
- Lifecycle:
304
- 1. \`figs login\` a human approves in a browser the token is theirs, never yours.
305
- 2. \`figs init --workspace <slug>\` done wrote config.json (your identity UUID) + this file.
306
- 3. author \`.figs/agent.json\` derive it from this repo's CLAUDE.md / MEMORY.md, then \`figs doctor\`.
307
- 4. \`figs push\` you now appear in the org chart, self-described.
313
+ - **Full guide + schema → ${endpoint}/llms.txt** — read this first.
314
+ - **Your contract → ./CONTRACT.md** what you publish / hold back (you + your user author it).
315
+ - **Identity config.json** (committed) · **charter → agent.json** (you write).
308
316
 
309
- Every run (wire this into your run loop, not a one-off):
310
- append a line to runs.jsonl · record blockers in asks.jsonl · copy reports into artifacts/ · \`figs push\`.
317
+ **Where are you?** You may be reading this at any time don't assume you're starting fresh. Run
318
+ \`figs status\` and check \`.figs/\` before acting:
319
+ • not set up yet → author \`.figs/agent.json\`, then \`figs doctor\` && \`figs push\` to appear (Identity).
320
+ • CONTRACT.md present → follow it; keep publishing what it says.
321
+ • no CONTRACT.md yet → agree with your user what work to surface (Activity) — see ${endpoint}/llms.txt.
311
322
 
312
- Commit config.json + agent.json (identity + charter). runs.jsonl / asks.jsonl / artifacts/ are a
313
- transient outbox gitignored, aggregated remotely. The sync is one-way and never deletes on the server.
323
+ Commit config.json + agent.json + CONTRACT.md + this file. runs.jsonl / asks.jsonl / artifacts/
324
+ are a gitignored outbox. The token is the human's job — never generate one yourself.
314
325
  `
315
326
  }
316
327
 
@@ -355,7 +366,7 @@ async function init() {
355
366
  writeFileSync(
356
367
  giPath,
357
368
  [
358
- "# Figs — commit config.json + agent.json (identity + charter).",
369
+ "# Figs — commit config.json + agent.json + CONTRACT.md + GUIDE.md.",
359
370
  "# Activity is a transient outbox: emitted per run, aggregated remotely.",
360
371
  "runs.jsonl",
361
372
  "asks.jsonl",
@@ -371,8 +382,9 @@ async function init() {
371
382
  `figs: ✓ .figs/config.json + .gitignore + GUIDE.md written (agentId ${agentId})`,
372
383
  )
373
384
  console.log(
374
- ` read .figs/GUIDE.md (full guide: ${endpoint}/llms.txt), author .figs/agent.json, then \`figs doctor\`.`,
385
+ ` Phase 1: author .figs/agent.json (your charter), then \`figs doctor\` && \`figs push\` to appear.`,
375
386
  )
387
+ console.log(` Full guide: ${endpoint}/llms.txt`)
376
388
  }
377
389
 
378
390
  /** Validate the local .figs/ payload against the contract — no write. */
@@ -445,8 +457,10 @@ async function push() {
445
457
  * The spine ingest is JSON-only; artifacts go to a separate endpoint that stores
446
458
  * them content-addressed (an unchanged file is skipped server-side). Content is
447
459
  * sent base64-encoded so any type — html, markdown, text, json, images — survives.
448
- * Files larger than ~3 MB are rejected by the server; compress images if needed.
449
- * Missing files are warned, not fatal.
460
+ * A **server rejection** (auth/size/etc.) is fatal: it prints and exits non-zero
461
+ * so the agent never believes a report published when it didn't (esp. the ~3 MB
462
+ * cap → 413). A **missing local file** is only a warning — that's the agent
463
+ * referencing an artifact it didn't actually produce, not a publish failure.
450
464
  */
451
465
  async function pushArtifacts(base, token, config, runs, asks) {
452
466
  const refNames = (asks ?? []).flatMap((a) =>
@@ -459,10 +473,13 @@ async function pushArtifacts(base, token, config, runs, asks) {
459
473
 
460
474
  let uploaded = 0
461
475
  let unchanged = 0
476
+ let missing = 0
477
+ let failed = 0
462
478
  for (const name of names) {
463
479
  const p = join(repoDir, "artifacts", name)
464
480
  if (!existsSync(p)) {
465
481
  console.warn(`figs: ! artifact missing, skipped: artifacts/${name}`)
482
+ missing++
466
483
  continue
467
484
  }
468
485
  const content = readFileSync(p).toString("base64")
@@ -477,8 +494,13 @@ async function pushArtifacts(base, token, config, runs, asks) {
477
494
  }),
478
495
  })
479
496
  if (!res.ok) {
480
- const t = await res.text()
481
- console.warn(`figs: ! artifact upload failed (${res.status}) ${name}: ${t}`)
497
+ const t = await res.text().catch(() => "")
498
+ const hint =
499
+ res.status === 413 ? " — too large (>3 MB); compress or split it" : ""
500
+ console.error(
501
+ `figs: ✗ artifact upload failed (${res.status}) ${name}${hint}${t ? `: ${t}` : ""}`,
502
+ )
503
+ failed++
482
504
  continue
483
505
  }
484
506
  const body = await res.json().catch(() => ({}))
@@ -486,8 +508,13 @@ async function pushArtifacts(base, token, config, runs, asks) {
486
508
  else uploaded++
487
509
  }
488
510
  console.log(
489
- `figs: ✓ artifacts — ${uploaded} uploaded, ${unchanged} unchanged`,
511
+ `figs: ${failed ? "✗" : ""} artifacts — ${uploaded} uploaded, ${unchanged} unchanged` +
512
+ (missing ? `, ${missing} missing` : "") +
513
+ (failed ? `, ${failed} failed` : ""),
490
514
  )
515
+ // The spine already landed; signal a non-zero exit so an agent's run loop can
516
+ // catch that an artifact the manager needs to read did not publish.
517
+ if (failed) process.exit(1)
491
518
  }
492
519
 
493
520
  function readJsonl(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@figs-so/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Figs CLI — publish your AI agent's state to Figs (figs.so). Run by the agent.",
5
5
  "type": "module",
6
6
  "bin": {