@figs-so/cli 0.1.14 → 0.1.16
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 +31 -17
- package/SPEC.md +8 -8
- package/figs.mjs +218 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,8 +6,9 @@ Figs is the open protocol — and the place — for how AI employees report to a
|
|
|
6
6
|
Every agent you run (Claude Code, Codex, Cursor) publishes what it owns, what it's done, and what it
|
|
7
7
|
needs from a person — into one shared view your whole team can see.
|
|
8
8
|
|
|
9
|
-
> **
|
|
10
|
-
> at **[app.figs.so](https://app.figs.so)** is the easiest place to read it; you can
|
|
9
|
+
> **The open standard for how AI employees report to humans.** The `.figs` format is that standard (this
|
|
10
|
+
> repo). The hosted app at **[app.figs.so](https://app.figs.so)** is the easiest place to read it; you can
|
|
11
|
+
> also self-host.
|
|
11
12
|
|
|
12
13
|
[](https://www.npmjs.com/package/@figs-so/cli)
|
|
13
14
|
· License: **MIT** (this repo — protocol + CLI) · The app: **AGPL-3.0**
|
|
@@ -26,17 +27,16 @@ what happened; you read Figs. And when an agent hits something only a human can
|
|
|
26
27
|
silently — it **hands off** to you.
|
|
27
28
|
|
|
28
29
|
We don't reinvent the agent. Your agent is already Claude Code / Codex / Cursor, and it's only getting
|
|
29
|
-
better. Figs is the human-facing layer on top: the one place a whole team can see the
|
|
30
|
+
better. Figs is the human-facing layer on top: the one place a whole team can see the fleet.
|
|
30
31
|
|
|
31
32
|
## Quickstart (60 seconds)
|
|
32
33
|
|
|
33
34
|
Run these from your agent's repo (or have the agent run them):
|
|
34
35
|
|
|
35
36
|
```bash
|
|
36
|
-
npx @figs-so/cli@latest login #
|
|
37
|
-
npx @figs-so/cli@latest
|
|
38
|
-
|
|
39
|
-
# describe the agent in .figs/agent.json — its name, mandate, what it owns
|
|
37
|
+
npx @figs-so/cli@latest login # opens your browser — sign up & approve (the agent never sees a token)
|
|
38
|
+
npx @figs-so/cli@latest init # scaffolds .figs/ — uses your only workspace (--workspace <slug> to pick)
|
|
39
|
+
# fill in .figs/agent.json — its name, mandate, what it owns (figs doctor flags any placeholders)
|
|
40
40
|
npx @figs-so/cli@latest push # publish → it appears in your org chart
|
|
41
41
|
```
|
|
42
42
|
|
|
@@ -51,8 +51,9 @@ SDK in your agent's code. From there you decide, deliberately, how much of its r
|
|
|
51
51
|
*rendered artifacts* (reports/charts shown in a sandboxed viewer). No display DSL to learn.
|
|
52
52
|
- **Identity is the agent's own.** An agent generates a UUID once; that UUID *is* its identity. Many people
|
|
53
53
|
can run the same agent (it's a repo) and their pushes aggregate.
|
|
54
|
-
- **You read it on Figs.** The hosted app turns the pushes into an org chart of your AI
|
|
55
|
-
view per agent, and
|
|
54
|
+
- **You read it on Figs.** The hosted app turns the pushes into an org chart of your AI employees, a glance
|
|
55
|
+
view per agent, and a **needs-you inbox** — the handoffs an employee flags for a human, answered when you
|
|
56
|
+
have time (a message, not a blocking gate).
|
|
56
57
|
|
|
57
58
|
The full `.figs` contract is specified in **[`SPEC.md`](./SPEC.md)** (`figs-spec v1`). Anyone can implement
|
|
58
59
|
it — that's the point of an open protocol.
|
|
@@ -62,11 +63,15 @@ it — that's the point of an open protocol.
|
|
|
62
63
|
`@figs-so/cli` (command `figs`) is zero-dependency, Node ≥ 18, and built to be run *by the agent*:
|
|
63
64
|
non-interactive, `--json` on read commands, and errors that say what to do next.
|
|
64
65
|
|
|
66
|
+
**Invoke it with `npx @figs-so/cli@latest <cmd>`** — no install needed; the `figs <cmd>` forms below
|
|
67
|
+
are shorthand for exactly that (always current, no version drift). Prefer a real local command?
|
|
68
|
+
`npm i -g @figs-so/cli`, then `figs <cmd>` directly.
|
|
69
|
+
|
|
65
70
|
| Command | What |
|
|
66
71
|
|---|---|
|
|
67
72
|
| `figs login` / `logout` | device-flow browser approve / remove local token |
|
|
68
73
|
| `figs workspaces [--json]` | list your workspaces (create one in the web app) |
|
|
69
|
-
| `figs init --workspace <slug
|
|
74
|
+
| `figs init [--workspace <slug>]` | generate identity + write `.figs/` (omit the flag: uses your only workspace, else lists them) |
|
|
70
75
|
| `figs doctor` | validate `.figs/` against the contract before pushing |
|
|
71
76
|
| `figs push` | one-way publish of `.figs/` |
|
|
72
77
|
| `figs status [--json]` | login / workspace / agent state |
|
|
@@ -76,7 +81,7 @@ Override the endpoint for local dev with `FIGS_ENDPOINT` (e.g. `http://localhost
|
|
|
76
81
|
|
|
77
82
|
## What Figs is — and is NOT
|
|
78
83
|
|
|
79
|
-
**Is:** the human-facing reporting + handoff layer for your
|
|
84
|
+
**Is:** the human-facing reporting + handoff layer for your fleet. The neutral, multiplayer place
|
|
80
85
|
that makes a fleet of agents *legible* to a whole team.
|
|
81
86
|
|
|
82
87
|
**Is NOT:**
|
|
@@ -90,18 +95,27 @@ that makes a fleet of agents *legible* to a whole team.
|
|
|
90
95
|
> at fleet scale — not a tamper-proof audit trail (agent state is self-reported). We're building in the
|
|
91
96
|
> open; expect rough edges and tell us where it breaks.
|
|
92
97
|
|
|
93
|
-
## Run it
|
|
98
|
+
## Run it
|
|
94
99
|
|
|
95
|
-
- **Hosted
|
|
96
|
-
|
|
97
|
-
bring your own Postgres + storage. See its README for setup.
|
|
100
|
+
- **Hosted:** [app.figs.so](https://app.figs.so) — sign in, create a workspace, push. The app is a hosted
|
|
101
|
+
product; the CLI + protocol in this repo are MIT and run anywhere.
|
|
98
102
|
|
|
99
103
|
## Licensing
|
|
100
104
|
|
|
101
105
|
- **This repo — the `.figs` protocol + the CLI: [MIT](./LICENSE).** Use it, embed it, build on it, emit
|
|
102
106
|
`.figs` from anything. Zero friction is the point.
|
|
103
|
-
- **The hosted app
|
|
104
|
-
|
|
107
|
+
- **The hosted app at [app.figs.so](https://app.figs.so) is a commercial product** (closed source). Your
|
|
108
|
+
data isn't locked in, though — it's `.figs`, an open format you can read or export anytime.
|
|
109
|
+
|
|
110
|
+
## The Figs ecosystem
|
|
111
|
+
|
|
112
|
+
Figs is one stack in three pieces — **build → report → govern**. Land on any repo; here's the whole picture:
|
|
113
|
+
|
|
114
|
+
| Layer | Repo | License | Role |
|
|
115
|
+
|---|---|---|---|
|
|
116
|
+
| 🏗️ Build | **[OpenFigs](https://github.com/figs-so/openfigs)** | MIT | build trustworthy back-office AI employees — conventions + skeleton, runtime-agnostic |
|
|
117
|
+
| 📤 Report | **[`.figs` + CLI](https://github.com/figs-so/figs)** | MIT | the open standard an agent reports its state in — **← you're here** |
|
|
118
|
+
| 👁️ Govern | **[Figs app](https://app.figs.so)** | hosted | the org chart + handoff inbox humans read |
|
|
105
119
|
|
|
106
120
|
## Links
|
|
107
121
|
|
package/SPEC.md
CHANGED
|
@@ -41,16 +41,17 @@ Non-secret. Pins one shared identity so many runners' pushes aggregate.
|
|
|
41
41
|
|---|---|---|
|
|
42
42
|
| `endpoint` | string (URL) | Where to publish (default `https://app.figs.so`). |
|
|
43
43
|
| `workspaceId` | UUID | The workspace this agent belongs to. |
|
|
44
|
-
| `agentId` | UUID | The agent's identity
|
|
44
|
+
| `agentId` | UUID | The agent's identity, generated once by `figs init`. The CLI attaches it as the agent's `id` on push (you don't hand-author `id` in `agent.json`). |
|
|
45
45
|
|
|
46
46
|
## 4. `agent.json` — the charter
|
|
47
47
|
|
|
48
|
-
The agent's self-description. Authoring this and publishing makes the agent *appear*.
|
|
49
|
-
|
|
48
|
+
The agent's self-description. Authoring this and publishing makes the agent *appear*. The only field you
|
|
49
|
+
author that's required is `name` — **do not hand-author `id`**: `figs init` mints it into `config.json` and
|
|
50
|
+
the CLI attaches it on push. Everything else is optional and rendered when present.
|
|
50
51
|
|
|
51
52
|
| Field | Type | Req | Meaning |
|
|
52
53
|
|---|---|:--:|---|
|
|
53
|
-
| `id` | UUID | ✓ | Identity
|
|
54
|
+
| `id` | UUID | ✓ | Identity. **Supplied from `config.json#agentId` by the CLI on push — not written in this file.** |
|
|
54
55
|
| `name` | string | ✓ | Display name. |
|
|
55
56
|
| `key` | string | | Display slug; derived from `name` if absent. |
|
|
56
57
|
| `type` | `"agent"` \| `"human"` | | Default `"agent"`. |
|
|
@@ -102,7 +103,7 @@ primitive** — the agent reached the edge of its autonomy.
|
|
|
102
103
|
| Field | Type | Req | Meaning |
|
|
103
104
|
|---|---|:--:|---|
|
|
104
105
|
| `id` | string | ✓ | Stable id (upsert key). |
|
|
105
|
-
| `type` | enum | ✓ | `blocked` \| `needs-decision` \| `confirm-assumption`
|
|
106
|
+
| `type` | enum | ✓ | `blocked` \| `needs-decision` \| `sign-off` \| `fyi`. `fyi` is a non-blocking heads-up (no decision needed). (`confirm-assumption` still validates but is **deprecated** — use `needs-decision` or `fyi`.) |
|
|
106
107
|
| `status` | `"open"` \| `"resolved"` | | Default `"open"`. |
|
|
107
108
|
| `title` | string | ✓ | The ask, in one line. |
|
|
108
109
|
| `unit` | string | | The `Unit.id` this concerns. |
|
|
@@ -174,9 +175,8 @@ Deliberately out of scope for v1, named here so implementers don't repurpose the
|
|
|
174
175
|
```
|
|
175
176
|
|
|
176
177
|
```jsonc
|
|
177
|
-
// .figs/agent.json
|
|
178
|
+
// .figs/agent.json (no `id` here — `figs init` puts it in config.json; the CLI attaches it on push)
|
|
178
179
|
{
|
|
179
|
-
"id": "…uuid… (== config.agentId)",
|
|
180
180
|
"name": "Reconciliation",
|
|
181
181
|
"type": "agent",
|
|
182
182
|
"role": "Reconciliation Officer",
|
|
@@ -210,7 +210,7 @@ Deliberately out of scope for v1, named here so implementers don't repurpose the
|
|
|
210
210
|
|
|
211
211
|
```jsonc
|
|
212
212
|
// .figs/asks.jsonl (one object per line)
|
|
213
|
-
{ "id": "acme-bridge", "ts": "2026-05-28T21:05:00Z", "type": "
|
|
213
|
+
{ "id": "acme-bridge", "ts": "2026-05-28T21:05:00Z", "type": "needs-decision", "status": "open", "unit": "acme",
|
|
214
214
|
"title": "No bridge rule for prefixed invoice numbers",
|
|
215
215
|
"found": "~180 rows can't be matched safely; guessing risks false matches.",
|
|
216
216
|
"need": "Confirm the bridge rule for prefixed invoice numbers.",
|
package/figs.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `figs` — the agent-side CLI (v1, zero-dependency).
|
|
4
4
|
*
|
|
5
5
|
* figs status show login / workspace / agent state [--json]
|
|
6
|
-
* figs login browser approve (device flow) — agent never sees the token
|
|
6
|
+
* figs login opens browser to approve (device flow) — agent never sees the token
|
|
7
7
|
* figs login <token> fallback: save a token you pasted (~/.figs/credentials.json)
|
|
8
8
|
* figs logout remove the locally-saved token (~/.figs/credentials.json)
|
|
9
9
|
* figs workspaces list the user's workspaces [--json]
|
|
@@ -35,8 +35,9 @@ import {
|
|
|
35
35
|
writeFileSync,
|
|
36
36
|
} from "node:fs"
|
|
37
37
|
import { homedir } from "node:os"
|
|
38
|
-
import { join } from "node:path"
|
|
38
|
+
import { basename, join } from "node:path"
|
|
39
39
|
import { randomUUID } from "node:crypto"
|
|
40
|
+
import { spawn } from "node:child_process"
|
|
40
41
|
|
|
41
42
|
// Single source of truth for the version: package.json (shipped alongside this
|
|
42
43
|
// file in the published package). One edit keeps `figs version`, the floor
|
|
@@ -72,7 +73,7 @@ const COMMANDS = {
|
|
|
72
73
|
flags: [],
|
|
73
74
|
desc: "log in — browser device-flow, or save a pasted token",
|
|
74
75
|
more: [
|
|
75
|
-
"no arg → device flow: a
|
|
76
|
+
"no arg → device flow: opens a browser for a human to approve (you never see the token).",
|
|
76
77
|
"<token> → save a token you already have to ~/.figs/credentials.json.",
|
|
77
78
|
],
|
|
78
79
|
eg: "figs login",
|
|
@@ -85,11 +86,13 @@ const COMMANDS = {
|
|
|
85
86
|
eg: "figs workspaces",
|
|
86
87
|
},
|
|
87
88
|
init: {
|
|
88
|
-
args: "--workspace <slug-or-id> [--endpoint <url>]",
|
|
89
|
+
args: "[--workspace <slug-or-id>] [--endpoint <url>]",
|
|
89
90
|
flags: ["--workspace", "--endpoint"],
|
|
90
|
-
desc: "
|
|
91
|
+
desc: "scaffold .figs/ here (identity + charter/contract/guide templates)",
|
|
91
92
|
more: [
|
|
92
93
|
"--workspace takes a slug (resolved to its UUID) or a raw UUID — get it from `figs workspaces`.",
|
|
94
|
+
"Omit --workspace and (logged in) it uses your only workspace, or lists them so you can re-run with one.",
|
|
95
|
+
"Never clobbers: an existing agent.json / CONTRACT.md / GUIDE.md / outbox is left exactly as-is.",
|
|
93
96
|
],
|
|
94
97
|
eg: "figs init --workspace acme-corp",
|
|
95
98
|
},
|
|
@@ -212,9 +215,9 @@ function cmpSemver(a, b) {
|
|
|
212
215
|
return 0
|
|
213
216
|
}
|
|
214
217
|
/**
|
|
215
|
-
* Cached (daily)
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
+
* Cached (daily) compatibility check — off the hot path. `hardFail` exits when
|
|
219
|
+
* below the server's compatible `min`. Network failure is ignored (never blocks
|
|
220
|
+
* on a transient outage).
|
|
218
221
|
*/
|
|
219
222
|
async function checkVersion({ force = false, hardFail = false } = {}) {
|
|
220
223
|
const cachePath = join(globalDir, "version-check.json")
|
|
@@ -233,14 +236,11 @@ async function checkVersion({ force = false, hardFail = false } = {}) {
|
|
|
233
236
|
}
|
|
234
237
|
}
|
|
235
238
|
const min = info?.cli?.min
|
|
236
|
-
const latest = info?.cli?.latest
|
|
237
239
|
// cmpSemver returns null on an unparseable version → skip (never fail closed).
|
|
238
240
|
if (min && cmpSemver(VERSION, min) === -1) {
|
|
239
241
|
const msg = `figs CLI ${VERSION} is below the minimum ${min} — upgrade: npx @figs-so/cli@latest`
|
|
240
242
|
if (hardFail) die(msg)
|
|
241
243
|
console.warn(`figs: ! ${msg}`)
|
|
242
|
-
} else if (latest && cmpSemver(VERSION, latest) === -1) {
|
|
243
|
-
console.warn(`figs: a newer CLI is available (${latest}) — npx @figs-so/cli@latest`)
|
|
244
244
|
}
|
|
245
245
|
}
|
|
246
246
|
|
|
@@ -311,9 +311,30 @@ function saveToken(token) {
|
|
|
311
311
|
}
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
// Best-effort: pop the approval page in the user's default browser so they only
|
|
315
|
+
// have to click Approve. Silent no-op when it can't (headless / remote / CI) —
|
|
316
|
+
// the link is always printed above as the fallback. Detached + unref'd so it
|
|
317
|
+
// never blocks or ties the polling loop to the browser process.
|
|
318
|
+
function openBrowser(url) {
|
|
319
|
+
try {
|
|
320
|
+
const [cmd, args] =
|
|
321
|
+
process.platform === "darwin"
|
|
322
|
+
? ["open", [url]]
|
|
323
|
+
: process.platform === "win32"
|
|
324
|
+
? ["cmd", ["/c", "start", "", url]]
|
|
325
|
+
: ["xdg-open", [url]]
|
|
326
|
+
const child = spawn(cmd, args, { stdio: "ignore", detached: true })
|
|
327
|
+
child.on("error", () => {}) // swallow ENOENT / no opener available
|
|
328
|
+
child.unref()
|
|
329
|
+
} catch {
|
|
330
|
+
/* ignore — the printed link is the fallback */
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
314
334
|
/**
|
|
315
|
-
* `figs login` → device flow
|
|
316
|
-
*
|
|
335
|
+
* `figs login` → device flow: opens the approval page in the user's browser
|
|
336
|
+
* (the printed link is the fallback); the human clicks Approve and the agent
|
|
337
|
+
* never handles the token. `figs login <token>` → save a pasted token (fallback).
|
|
317
338
|
*/
|
|
318
339
|
async function login(token) {
|
|
319
340
|
if (token) {
|
|
@@ -325,9 +346,10 @@ async function login(token) {
|
|
|
325
346
|
const start = await request("POST", "/api/device/start")
|
|
326
347
|
if (!start.ok) die(`could not start login (${start.status})`)
|
|
327
348
|
const d = start.data
|
|
328
|
-
console.log("figs: to
|
|
329
|
-
console.log(` ${d.verification_uri_complete}`)
|
|
349
|
+
console.log("figs: opening your browser to approve this CLI — just click Approve there.")
|
|
350
|
+
console.log(` if it doesn't open, visit: ${d.verification_uri_complete}`)
|
|
330
351
|
console.log(` (or go to ${d.verification_uri} and enter code: ${d.user_code})`)
|
|
352
|
+
openBrowser(d.verification_uri_complete)
|
|
331
353
|
console.log("figs: waiting for approval…")
|
|
332
354
|
|
|
333
355
|
const deadline = Date.now() + (d.expires_in ?? 600) * 1000
|
|
@@ -339,6 +361,7 @@ async function login(token) {
|
|
|
339
361
|
if (status === "approved" && r.data.token) {
|
|
340
362
|
saveToken(r.data.token)
|
|
341
363
|
console.log("figs: ✓ authorized — token saved to ~/.figs/credentials.json")
|
|
364
|
+
console.log("figs: next — run `figs init` to scaffold .figs/ here")
|
|
342
365
|
return
|
|
343
366
|
}
|
|
344
367
|
if (status === "denied") die("authorization denied")
|
|
@@ -515,18 +538,92 @@ are a gitignored outbox. The token is the human's job — never generate one you
|
|
|
515
538
|
`
|
|
516
539
|
}
|
|
517
540
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
541
|
+
/**
|
|
542
|
+
* A starter `agent.json` — written by `figs init` only when none exists. The
|
|
543
|
+
* `<…>` values are placeholders the agent fills in by reading its own repo;
|
|
544
|
+
* `figs doctor` refuses to bless a charter that still has them. `name` defaults
|
|
545
|
+
* to the folder name (a sensible first guess), `id` is intentionally absent —
|
|
546
|
+
* the CLI attaches the identity UUID from config.json on push.
|
|
547
|
+
*/
|
|
548
|
+
function agentJsonStub(name) {
|
|
549
|
+
return (
|
|
550
|
+
JSON.stringify(
|
|
551
|
+
{
|
|
552
|
+
name,
|
|
553
|
+
role: "<one line — what you are>",
|
|
554
|
+
status: "in_dev",
|
|
555
|
+
mandate: "<one sentence — what you are accountable for>",
|
|
556
|
+
org: { department: "<your team / department>" },
|
|
557
|
+
runtime: "<what runs you, e.g. Claude Code>",
|
|
558
|
+
cadence: "<on-demand · weekly · monthly · …>",
|
|
559
|
+
responsibilities: ["<an area of work you own — list a few, or use steps>"],
|
|
560
|
+
properties: [{ k: "<fact>", v: "<value>" }],
|
|
561
|
+
},
|
|
562
|
+
null,
|
|
563
|
+
2,
|
|
564
|
+
) + "\n"
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/** A starter activity contract — written by `figs init` only when none exists. */
|
|
569
|
+
function contractStub(name) {
|
|
570
|
+
return `# Activity contract — ${name} on Figs
|
|
571
|
+
|
|
572
|
+
What this agent surfaces to Figs vs. holds back. **Agree it with your user** — this is the
|
|
573
|
+
deliberate Activity step, not something to do mechanically. See \`.figs/GUIDE.md\` for the why.
|
|
574
|
+
|
|
575
|
+
> **Maintain:** edit when the surfacing agreement changes (a new stream, a sensitivity change).
|
|
576
|
+
> Keep it honest to what you actually push.
|
|
577
|
+
|
|
578
|
+
## What I surface
|
|
579
|
+
|
|
580
|
+
| Stream | Surface? | Content |
|
|
581
|
+
|--------|----------|---------|
|
|
582
|
+
| **runs** | <yes/no> | one line per run — what I did, de-identified scope, the headline result + status. |
|
|
583
|
+
| **artifacts** | <yes/no> | the report(s) a run produced. |
|
|
584
|
+
| **asks** | when real | genuine blockers / decisions / sign-offs / FYIs for my manager. 0 is a fine number. |
|
|
585
|
+
|
|
586
|
+
## What I never surface
|
|
587
|
+
|
|
588
|
+
Raw user content — ever. Plus, for this agent: <anything sensitive to its domain>. Use
|
|
589
|
+
**de-identified labels** (\`<scope>-01\`), never customer or system names.
|
|
590
|
+
`
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Find string values still left as `<…>` template placeholders, with their JSON
|
|
595
|
+
* path. Used by `figs doctor` to block publishing a half-filled charter. Matches
|
|
596
|
+
* a value that is *entirely* a placeholder (e.g. "<one line — what you are>") so
|
|
597
|
+
* real content containing stray angle brackets isn't flagged.
|
|
598
|
+
*/
|
|
599
|
+
function findPlaceholders(obj) {
|
|
600
|
+
const out = []
|
|
601
|
+
const walk = (v, path) => {
|
|
602
|
+
if (typeof v === "string") {
|
|
603
|
+
if (/^<.*>$/.test(v.trim())) out.push({ path: path || "(root)", value: v })
|
|
604
|
+
} else if (Array.isArray(v)) {
|
|
605
|
+
v.forEach((x, i) => walk(x, `${path}[${i}]`))
|
|
606
|
+
} else if (v && typeof v === "object") {
|
|
607
|
+
for (const [k, x] of Object.entries(v)) walk(x, path ? `${path}.${k}` : k)
|
|
608
|
+
}
|
|
522
609
|
}
|
|
523
|
-
|
|
610
|
+
walk(obj, "")
|
|
611
|
+
return out
|
|
612
|
+
}
|
|
524
613
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
614
|
+
/**
|
|
615
|
+
* Resolve which workspace this `.figs/` belongs to. `--workspace` is optional:
|
|
616
|
+
* - given a UUID → use it as-is (no network).
|
|
617
|
+
* - given a slug → resolve to its UUID via the API (needs auth).
|
|
618
|
+
* - omitted → reuse the one already in config.json (idempotent re-init);
|
|
619
|
+
* else use the user's only workspace (logged, so it's visible);
|
|
620
|
+
* else list them and have the agent re-run with one (when
|
|
621
|
+
* there's a real choice, the agent drives it — we never guess).
|
|
622
|
+
* Returns the workspace UUID, or exits with an actionable message.
|
|
623
|
+
*/
|
|
624
|
+
async function resolveWorkspaceId(workspaceArg, endpoint) {
|
|
625
|
+
if (workspaceArg) {
|
|
626
|
+
if (isUuid(workspaceArg)) return workspaceArg
|
|
530
627
|
if (!getToken()) {
|
|
531
628
|
die("not logged in — run `figs login` first (resolving a workspace slug needs auth; or pass the workspace UUID)")
|
|
532
629
|
}
|
|
@@ -536,12 +633,40 @@ async function init() {
|
|
|
536
633
|
}
|
|
537
634
|
const list = r.data.workspaces ?? []
|
|
538
635
|
const match = list.find((w) => w.slug === workspaceArg || w.id === workspaceArg)
|
|
539
|
-
if (!match) {
|
|
540
|
-
|
|
541
|
-
}
|
|
542
|
-
workspaceId = match.id
|
|
636
|
+
if (!match) die(`no workspace matching "${workspaceArg}" — run \`figs workspaces\` to see yours`)
|
|
637
|
+
return match.id
|
|
543
638
|
}
|
|
544
639
|
|
|
640
|
+
// No --workspace: a re-init keeps the workspace already on file.
|
|
641
|
+
const existing = readJson(join(repoDir, "config.json"), null)
|
|
642
|
+
if (existing?.workspaceId) return existing.workspaceId
|
|
643
|
+
|
|
644
|
+
// First-time init with no workspace named — use the only one, else list them.
|
|
645
|
+
if (!getToken()) {
|
|
646
|
+
die("which workspace? run `figs login` first so I can list them, then `figs init --workspace <slug>` (or pass a workspace UUID directly)")
|
|
647
|
+
}
|
|
648
|
+
const r = await request("GET", "/api/workspaces", null, getToken())
|
|
649
|
+
if (!r.ok) die(`could not list workspaces (${r.status}): ${r.data.error ?? r.data.raw ?? ""}`)
|
|
650
|
+
const list = r.data.workspaces ?? []
|
|
651
|
+
if (list.length === 0) {
|
|
652
|
+
die(`no workspaces yet — create one at ${endpoint}, then re-run \`figs init --workspace <slug>\``)
|
|
653
|
+
}
|
|
654
|
+
if (list.length === 1) {
|
|
655
|
+
console.log(`figs: using workspace ${list[0].slug} (${list[0].name})`)
|
|
656
|
+
return list[0].id
|
|
657
|
+
}
|
|
658
|
+
console.log("figs: which workspace? re-run init with one of these:")
|
|
659
|
+
for (const w of list) console.log(` figs init --workspace ${w.slug} (${w.name})`)
|
|
660
|
+
process.exit(1)
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function init() {
|
|
664
|
+
const endpoint = (flag("--endpoint") || resolveEndpoint()).replace(/\/+$/, "")
|
|
665
|
+
const workspaceId = await resolveWorkspaceId(flag("--workspace"), endpoint)
|
|
666
|
+
|
|
667
|
+
// config.json (identity + destination) is always (re)written — it's the one
|
|
668
|
+
// file the CLI owns. A re-init reuses the existing identity UUID so every
|
|
669
|
+
// runner of this repo pushes to the same agent.
|
|
545
670
|
const existing = readJson(join(repoDir, "config.json"), null)
|
|
546
671
|
const agentId = existing?.agentId || randomUUID()
|
|
547
672
|
mkdirSync(repoDir, { recursive: true })
|
|
@@ -549,48 +674,87 @@ async function init() {
|
|
|
549
674
|
join(repoDir, "config.json"),
|
|
550
675
|
JSON.stringify({ endpoint, workspaceId, agentId }, null, 2) + "\n",
|
|
551
676
|
)
|
|
552
|
-
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
"artifacts/",
|
|
564
|
-
"credentials.json",
|
|
565
|
-
"",
|
|
566
|
-
].join("\n"),
|
|
567
|
-
)
|
|
677
|
+
|
|
678
|
+
// Everything else is scaffolded create-if-missing — `figs init` gives a fresh
|
|
679
|
+
// repo a complete, ready-to-fill `.figs/`, and never clobbers an agent's
|
|
680
|
+
// authored charter/contract/guide or its activity outbox.
|
|
681
|
+
const created = []
|
|
682
|
+
const ensure = (rel, contents) => {
|
|
683
|
+
const p = join(repoDir, rel)
|
|
684
|
+
if (existsSync(p)) return false
|
|
685
|
+
writeFileSync(p, contents)
|
|
686
|
+
created.push(rel)
|
|
687
|
+
return true
|
|
568
688
|
}
|
|
569
|
-
|
|
689
|
+
ensure(
|
|
690
|
+
".gitignore",
|
|
691
|
+
[
|
|
692
|
+
"# Figs — commit config.json + agent.json + CONTRACT.md + GUIDE.md.",
|
|
693
|
+
"# Activity is a transient outbox: emitted per run, aggregated remotely.",
|
|
694
|
+
"runs.jsonl",
|
|
695
|
+
"asks.jsonl",
|
|
696
|
+
"artifacts/",
|
|
697
|
+
"credentials.json",
|
|
698
|
+
"",
|
|
699
|
+
].join("\n"),
|
|
700
|
+
)
|
|
701
|
+
ensure("GUIDE.md", guideStub(endpoint))
|
|
702
|
+
const name = basename(process.cwd())
|
|
703
|
+
const charterCreated = ensure("agent.json", agentJsonStub(name))
|
|
704
|
+
ensure("CONTRACT.md", contractStub(name))
|
|
705
|
+
ensure("runs.jsonl", "")
|
|
706
|
+
ensure("asks.jsonl", "")
|
|
707
|
+
mkdirSync(join(repoDir, "artifacts"), { recursive: true })
|
|
570
708
|
|
|
571
709
|
console.log(
|
|
572
|
-
`figs: ✓ .figs/
|
|
710
|
+
`figs: ✓ .figs/ ready — config.json written (agentId ${agentId}, workspace ${workspaceId})`,
|
|
573
711
|
)
|
|
712
|
+
if (created.length) console.log(` scaffolded: ${created.join(", ")}`)
|
|
713
|
+
if (charterCreated) {
|
|
714
|
+
console.log(
|
|
715
|
+
" Phase 1: fill in .figs/agent.json — it's a template; replace the <…> placeholders",
|
|
716
|
+
)
|
|
717
|
+
console.log(
|
|
718
|
+
" (`figs doctor` flags any you miss), then `figs doctor` && `figs push` to appear.",
|
|
719
|
+
)
|
|
720
|
+
} else {
|
|
721
|
+
console.log(
|
|
722
|
+
" Your charter (.figs/agent.json) is already here — `figs doctor` && `figs push` to publish.",
|
|
723
|
+
)
|
|
724
|
+
}
|
|
574
725
|
console.log(
|
|
575
|
-
|
|
726
|
+
" Anchor Figs in the file you load every session (CLAUDE.md/AGENTS.md/…): paste the",
|
|
576
727
|
)
|
|
728
|
+
console.log(` figs:begin block from ${endpoint}/llms.txt, or future sessions forget Figs.`)
|
|
577
729
|
console.log(
|
|
578
|
-
|
|
730
|
+
" Commit config.json + agent.json + CONTRACT.md + GUIDE.md; never commit credentials.json.",
|
|
579
731
|
)
|
|
580
732
|
console.log(` Full guide: ${endpoint}/llms.txt`)
|
|
581
733
|
}
|
|
582
734
|
|
|
583
735
|
/** Validate the local .figs/ payload against the contract — no write. */
|
|
584
736
|
async function doctor() {
|
|
585
|
-
|
|
586
|
-
if (!existsSync(repoDir)) die("no .figs/ here — run `figs init
|
|
737
|
+
// Local checks first (no token/network needed) — fail fast and offline.
|
|
738
|
+
if (!existsSync(repoDir)) die("no .figs/ here — run `figs init` first")
|
|
587
739
|
const config = readJson(join(repoDir, "config.json"), {})
|
|
588
740
|
if (!config.workspaceId || !config.agentId) {
|
|
589
|
-
die("config missing workspaceId/agentId — run `figs init
|
|
741
|
+
die("config missing workspaceId/agentId — run `figs init`")
|
|
590
742
|
}
|
|
591
743
|
const agentJson = readJson(join(repoDir, "agent.json"), null)
|
|
592
744
|
if (!agentJson) die("missing .figs/agent.json — author it first (see .figs/GUIDE.md)")
|
|
593
745
|
|
|
746
|
+
// Refuse to bless a charter that still has `<…>` template placeholders — `figs
|
|
747
|
+
// init` scaffolds them, and pushing them would publish "<one line — what you
|
|
748
|
+
// are>" to the org chart. This is the "not ready to push" signal.
|
|
749
|
+
const placeholders = findPlaceholders(agentJson)
|
|
750
|
+
if (placeholders.length) {
|
|
751
|
+
console.log("figs: ✗ .figs/agent.json still has template placeholders — fill these in before pushing:")
|
|
752
|
+
for (const p of placeholders) console.log(` ${p.path}: ${p.value}`)
|
|
753
|
+
console.log(" (replace the <…> values by reading your own repo, then re-run `figs doctor`)")
|
|
754
|
+
process.exit(1)
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
if (!getToken()) die("not logged in — run `figs login`")
|
|
594
758
|
const r = await api("POST", "/api/validate", {
|
|
595
759
|
workspaceId: config.workspaceId,
|
|
596
760
|
agent: { ...agentJson, id: config.agentId },
|
|
@@ -621,11 +785,11 @@ async function push() {
|
|
|
621
785
|
if (!token) die("not logged in — run `figs login` (or set FIGS_TOKEN)")
|
|
622
786
|
await checkVersion({ hardFail: true })
|
|
623
787
|
if (!existsSync(repoDir)) {
|
|
624
|
-
die("no .figs/ here — run `figs init
|
|
788
|
+
die("no .figs/ here — run `figs init` first")
|
|
625
789
|
}
|
|
626
790
|
const config = readJson(join(repoDir, "config.json"), {})
|
|
627
791
|
if (!config.workspaceId || !config.agentId) {
|
|
628
|
-
die("config missing workspaceId/agentId — run `figs init
|
|
792
|
+
die("config missing workspaceId/agentId — run `figs init`")
|
|
629
793
|
}
|
|
630
794
|
const endpoint =
|
|
631
795
|
process.env.FIGS_ENDPOINT || config.endpoint || DEFAULT_ENDPOINT
|