@figs-so/cli 0.1.13 → 0.1.15
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/LICENSE +21 -0
- package/README.md +112 -24
- package/SPEC.md +220 -0
- package/figs.mjs +219 -56
- package/package.json +11 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wayne Hsu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,38 +1,126 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Figs
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
manager's window into the agents a company runs as back-office employees.
|
|
3
|
+
**Your AI employees do the work. Figs shows you what they did — and tells you when they need you.**
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Figs is the open protocol — and the place — for how AI employees report to and hand off work to humans.
|
|
6
|
+
Every agent you run (Claude Code, Codex, Cursor) publishes what it owns, what it's done, and what it
|
|
7
|
+
needs from a person — into one shared view your whole team can see.
|
|
8
8
|
|
|
9
|
-
|
|
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.
|
|
12
|
+
|
|
13
|
+
[](https://www.npmjs.com/package/@figs-so/cli)
|
|
14
|
+
· License: **MIT** (this repo — protocol + CLI) · The app: **AGPL-3.0**
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
You started with one agent. You watched its console. Now you're running five — soon thirty — and you
|
|
21
|
+
**can't keep thirty terminals in your head.** You don't actually know what your agents are doing, what
|
|
22
|
+
they've shipped, or which one is stuck waiting on you.
|
|
23
|
+
|
|
24
|
+
Figs treats each agent as what it's becoming: an **employee.** Not a log stream, not a trace — a worker
|
|
25
|
+
with a mandate that does its job and *reports back.* You stop reading consoles and codebases to find out
|
|
26
|
+
what happened; you read Figs. And when an agent hits something only a human can decide, it doesn't fail
|
|
27
|
+
silently — it **hands off** to you.
|
|
28
|
+
|
|
29
|
+
We don't reinvent the agent. Your agent is already Claude Code / Codex / Cursor, and it's only getting
|
|
30
|
+
better. Figs is the human-facing layer on top: the one place a whole team can see the fleet.
|
|
31
|
+
|
|
32
|
+
## Quickstart (60 seconds)
|
|
33
|
+
|
|
34
|
+
Run these from your agent's repo (or have the agent run them):
|
|
10
35
|
|
|
11
36
|
```bash
|
|
12
|
-
npx @figs-so/cli@latest login # browser approve (
|
|
13
|
-
npx @figs-so/cli@latest workspaces #
|
|
14
|
-
npx @figs-so/cli@latest init --workspace <slug> #
|
|
15
|
-
|
|
16
|
-
npx @figs-so/cli@latest push # publish
|
|
37
|
+
npx @figs-so/cli@latest login # opens your browser to approve (the agent never sees a token)
|
|
38
|
+
npx @figs-so/cli@latest workspaces # find your workspace slug
|
|
39
|
+
npx @figs-so/cli@latest init --workspace <slug> # scaffolds .figs/ (identity + a charter template)
|
|
40
|
+
# fill in .figs/agent.json — its name, mandate, what it owns (figs doctor flags any placeholders)
|
|
41
|
+
npx @figs-so/cli@latest push # publish → it appears in your org chart
|
|
17
42
|
```
|
|
18
43
|
|
|
19
|
-
|
|
20
|
-
|
|
44
|
+
That's it — your agent now shows up at **[app.figs.so](https://app.figs.so)**. No instrumentation, no
|
|
45
|
+
SDK in your agent's code. From there you decide, deliberately, how much of its real work to surface.
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
- **Local-first, one-way.** Your agent writes a small **`.figs/`** folder and runs `figs push`. Figs is a
|
|
50
|
+
**read-only mirror** — it never writes back into your agent or your repo.
|
|
51
|
+
- **Two things only:** *structured state* (the agent's charter + its runs, asks, and artifacts) and
|
|
52
|
+
*rendered artifacts* (reports/charts shown in a sandboxed viewer). No display DSL to learn.
|
|
53
|
+
- **Identity is the agent's own.** An agent generates a UUID once; that UUID *is* its identity. Many people
|
|
54
|
+
can run the same agent (it's a repo) and their pushes aggregate.
|
|
55
|
+
- **You read it on Figs.** The hosted app turns the pushes into an org chart of your AI employees, a glance
|
|
56
|
+
view per agent, and a **needs-you inbox** — the handoffs an employee flags for a human, answered when you
|
|
57
|
+
have time (a message, not a blocking gate).
|
|
58
|
+
|
|
59
|
+
The full `.figs` contract is specified in **[`SPEC.md`](./SPEC.md)** (`figs-spec v1`). Anyone can implement
|
|
60
|
+
it — that's the point of an open protocol.
|
|
21
61
|
|
|
22
|
-
|
|
62
|
+
### The CLI
|
|
63
|
+
|
|
64
|
+
`@figs-so/cli` (command `figs`) is zero-dependency, Node ≥ 18, and built to be run *by the agent*:
|
|
65
|
+
non-interactive, `--json` on read commands, and errors that say what to do next.
|
|
66
|
+
|
|
67
|
+
**Invoke it with `npx @figs-so/cli@latest <cmd>`** — no install needed; the `figs <cmd>` forms below
|
|
68
|
+
are shorthand for exactly that (always current, no version drift). Prefer a real local command?
|
|
69
|
+
`npm i -g @figs-so/cli`, then `figs <cmd>` directly.
|
|
23
70
|
|
|
24
71
|
| Command | What |
|
|
25
72
|
|---|---|
|
|
26
|
-
| `figs
|
|
27
|
-
| `figs
|
|
28
|
-
| `figs
|
|
29
|
-
| `figs
|
|
30
|
-
| `figs init --workspace <slug-or-id>` | resolve the workspace, generate identity UUID + config + pointer GUIDE.md |
|
|
31
|
-
| `figs doctor` | validate `.figs/` against the contract |
|
|
73
|
+
| `figs login` / `logout` | device-flow browser approve / remove local token |
|
|
74
|
+
| `figs workspaces [--json]` | list your workspaces (create one in the web app) |
|
|
75
|
+
| `figs init --workspace <slug>` | generate identity + write `.figs/` |
|
|
76
|
+
| `figs doctor` | validate `.figs/` against the contract before pushing |
|
|
32
77
|
| `figs push` | one-way publish of `.figs/` |
|
|
33
|
-
| `figs
|
|
34
|
-
| `figs help [<command>]` | usage (
|
|
78
|
+
| `figs status [--json]` | login / workspace / agent state |
|
|
79
|
+
| `figs help [<command>]` | usage (`-h`/`--help` on any command; `-v` for version) |
|
|
80
|
+
|
|
81
|
+
Override the endpoint for local dev with `FIGS_ENDPOINT` (e.g. `http://localhost:3000`).
|
|
82
|
+
|
|
83
|
+
## What Figs is — and is NOT
|
|
84
|
+
|
|
85
|
+
**Is:** the human-facing reporting + handoff layer for your fleet. The neutral, multiplayer place
|
|
86
|
+
that makes a fleet of agents *legible* to a whole team.
|
|
87
|
+
|
|
88
|
+
**Is NOT:**
|
|
89
|
+
- ❌ **An agent / framework / orchestrator** — we wrap the dominant ones; we don't compete with them.
|
|
90
|
+
- ❌ **Observability / a trace viewer** — the frame is an *employee reporting to humans*, not telemetry
|
|
91
|
+
for engineers.
|
|
92
|
+
- ❌ **A control plane (yet)** — today it's one-way (report + hand off). Two-way (answer-down, sign-off) is
|
|
93
|
+
on the roadmap. To act on a handoff today, you still go to the agent's own console.
|
|
94
|
+
|
|
95
|
+
> **Honest status:** Figs is **early** and in active dogfooding. Today's value is *visibility/legibility*
|
|
96
|
+
> at fleet scale — not a tamper-proof audit trail (agent state is self-reported). We're building in the
|
|
97
|
+
> open; expect rough edges and tell us where it breaks.
|
|
98
|
+
|
|
99
|
+
## Run it
|
|
100
|
+
|
|
101
|
+
- **Hosted:** [app.figs.so](https://app.figs.so) — sign in, create a workspace, push. The app is a hosted
|
|
102
|
+
product; the CLI + protocol in this repo are MIT and run anywhere.
|
|
103
|
+
|
|
104
|
+
## Licensing
|
|
105
|
+
|
|
106
|
+
- **This repo — the `.figs` protocol + the CLI: [MIT](./LICENSE).** Use it, embed it, build on it, emit
|
|
107
|
+
`.figs` from anything. Zero friction is the point.
|
|
108
|
+
- **The hosted app at [app.figs.so](https://app.figs.so) is a commercial product** (closed source). Your
|
|
109
|
+
data isn't locked in, though — it's `.figs`, an open format you can read or export anytime.
|
|
110
|
+
|
|
111
|
+
## The Figs ecosystem
|
|
112
|
+
|
|
113
|
+
Figs is one stack in three pieces — **build → report → govern**. Land on any repo; here's the whole picture:
|
|
114
|
+
|
|
115
|
+
| Layer | Repo | License | Role |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| 🏗️ Build | **[OpenFigs](https://github.com/figs-so/openfigs)** | MIT | build trustworthy back-office AI employees — conventions + skeleton, runtime-agnostic |
|
|
118
|
+
| 📤 Report | **[`.figs` + CLI](https://github.com/figs-so/figs)** | MIT | the open standard an agent reports its state in — **← you're here** |
|
|
119
|
+
| 👁️ Govern | **[Figs app](https://app.figs.so)** | hosted | the org chart + handoff inbox humans read |
|
|
35
120
|
|
|
36
|
-
|
|
121
|
+
## Links
|
|
37
122
|
|
|
38
|
-
|
|
123
|
+
- 🌐 Landing: **[figs.so](https://figs.so)**
|
|
124
|
+
- 🖥️ App: **[app.figs.so](https://app.figs.so)**
|
|
125
|
+
- 📦 CLI: **[@figs-so/cli](https://www.npmjs.com/package/@figs-so/cli)**
|
|
126
|
+
- 📄 Protocol: **[`SPEC.md`](./SPEC.md)**
|
package/SPEC.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# The `.figs` Protocol — `figs-spec v1`
|
|
2
|
+
|
|
3
|
+
> **Status:** v1 — minimal and stable. This spec defines the `.figs/` folder an AI agent writes and how
|
|
4
|
+
> it is published. It is deliberately small: it describes *reporting* (agent → human), which is all v1
|
|
5
|
+
> covers. Two-way (answers/sign-off flowing back to the agent) is **reserved for a future version** — see
|
|
6
|
+
> [Reserved](#reserved-not-in-v1). Licensed **MIT** — implement it in anything.
|
|
7
|
+
|
|
8
|
+
## 1. Design principles
|
|
9
|
+
|
|
10
|
+
- **One-way.** An agent *publishes* its state. A Figs reader is a **read-only mirror** — it never writes
|
|
11
|
+
back into the agent or its repo.
|
|
12
|
+
- **Local-first.** The agent owns a `.figs/` folder on disk. Publishing is an explicit act (`push`), not a
|
|
13
|
+
live connection.
|
|
14
|
+
- **Upsert-only.** Publishing inserts or updates records by their `id`; it **never deletes** remote rows.
|
|
15
|
+
The remote is a durable record; the local folder is a transient outbox.
|
|
16
|
+
- **Two content modes, no display language.** Everything is either *structured state* (JSON/JSONL we
|
|
17
|
+
describe below, rendered by fixed components) or a *rendered artifact* (a file shown in a sandboxed
|
|
18
|
+
viewer). There is no layout/templating DSL.
|
|
19
|
+
- **Self-describing identity.** An agent generates its own UUID once; that UUID *is* its identity. The same
|
|
20
|
+
agent (a repo) may be run by many people; their pushes aggregate under that one identity.
|
|
21
|
+
|
|
22
|
+
## 2. Folder layout
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
.figs/
|
|
26
|
+
├── config.json # identity + destination (committed, non-secret)
|
|
27
|
+
├── agent.json # the charter — who this agent is (committed)
|
|
28
|
+
├── runs.jsonl # activity log, one JSON object per line (outbox; gitignored)
|
|
29
|
+
├── asks.jsonl # things needing a human, one per line (outbox; gitignored)
|
|
30
|
+
└── artifacts/ # files referenced by runs/asks (outbox; gitignored)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Commit** `config.json` + `agent.json` (identity + charter). The activity files (`runs.jsonl`,
|
|
34
|
+
`asks.jsonl`, `artifacts/`) are a transient outbox and are typically gitignored.
|
|
35
|
+
|
|
36
|
+
## 3. `config.json` — identity + destination
|
|
37
|
+
|
|
38
|
+
Non-secret. Pins one shared identity so many runners' pushes aggregate.
|
|
39
|
+
|
|
40
|
+
| Field | Type | Notes |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `endpoint` | string (URL) | Where to publish (default `https://app.figs.so`). |
|
|
43
|
+
| `workspaceId` | UUID | The workspace this agent belongs to. |
|
|
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
|
+
|
|
46
|
+
## 4. `agent.json` — the charter
|
|
47
|
+
|
|
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.
|
|
51
|
+
|
|
52
|
+
| Field | Type | Req | Meaning |
|
|
53
|
+
|---|---|:--:|---|
|
|
54
|
+
| `id` | UUID | ✓ | Identity. **Supplied from `config.json#agentId` by the CLI on push — not written in this file.** |
|
|
55
|
+
| `name` | string | ✓ | Display name. |
|
|
56
|
+
| `key` | string | | Display slug; derived from `name` if absent. |
|
|
57
|
+
| `type` | `"agent"` \| `"human"` | | Default `"agent"`. |
|
|
58
|
+
| `avatar` | `{ seed: string }` | | Seed for the generated avatar. |
|
|
59
|
+
| `role` | string | | Short title, e.g. "Reconciliation Officer". |
|
|
60
|
+
| `status` | string | | Free-text lifecycle, e.g. `in_dev`, `active`. |
|
|
61
|
+
| `org` | `{ department?: string }` | | `department` groups the agent into an org-chart column. |
|
|
62
|
+
| `runtime` | string | | What runs it, e.g. "Claude Code". |
|
|
63
|
+
| `cadence` | string | | How often it runs, e.g. "Monthly". |
|
|
64
|
+
| `mandate` | string | | One-paragraph statement of what it's responsible for. |
|
|
65
|
+
| `steps` | string[] | | **Ordered** procedure (numbered render). For pipeline-shaped agents. |
|
|
66
|
+
| `responsibilities` | string[] | | **Unordered** areas of work (bulleted render). For broad/mission agents. |
|
|
67
|
+
| `properties` | `{ k, v }[]` | | Freeform catch-all for facts with no dedicated field. Keep keys short, values single-line. Don't duplicate first-class fields. |
|
|
68
|
+
| `units` | `Unit[]` | | The instances/things the agent operates on (see below). |
|
|
69
|
+
|
|
70
|
+
Use **`steps`** *or* **`responsibilities`** depending on shape — a fixed pipeline vs. a set of work areas.
|
|
71
|
+
|
|
72
|
+
### 4.1 `Unit` — a thing the agent operates on
|
|
73
|
+
|
|
74
|
+
| Field | Type | Req | Meaning |
|
|
75
|
+
|---|---|:--:|---|
|
|
76
|
+
| `id` | string | ✓ | Stable id (referenced by `runs`/`asks` via `unit`). |
|
|
77
|
+
| `name` | string | ✓ | Display name. |
|
|
78
|
+
| `subtitle` | string | | |
|
|
79
|
+
| `status` | string | | Current one-line state. |
|
|
80
|
+
| `period` | string | | The period in view, e.g. `2025-11`. |
|
|
81
|
+
| `detail` | string | | |
|
|
82
|
+
| `stats` | `{ l, v }[]` | | Labelled values (`l` = label, `v` = value). |
|
|
83
|
+
|
|
84
|
+
## 5. `runs.jsonl` — activity
|
|
85
|
+
|
|
86
|
+
One JSON object per line (JSON Lines). Each is something the agent did.
|
|
87
|
+
|
|
88
|
+
| Field | Type | Req | Meaning |
|
|
89
|
+
|---|---|:--:|---|
|
|
90
|
+
| `id` | string | ✓ | Stable id (upsert key). |
|
|
91
|
+
| `ts` | string (ISO-8601 w/ offset) | ✓ | When it ran, e.g. `2026-05-28T23:41:26Z`. |
|
|
92
|
+
| `unit` | string | | The `Unit.id` this run is about. |
|
|
93
|
+
| `period` | string | | |
|
|
94
|
+
| `result` | string | | One-line outcome. |
|
|
95
|
+
| `status` | `"ok"` \| `"warn"` \| `"fail"` | | Default `"ok"`. |
|
|
96
|
+
| `artifact` | string | | File name under `artifacts/` to attach. |
|
|
97
|
+
|
|
98
|
+
## 6. `asks.jsonl` — handoffs to a human
|
|
99
|
+
|
|
100
|
+
One JSON object per line. Each is something the agent needs a person to resolve. **This is the handoff
|
|
101
|
+
primitive** — the agent reached the edge of its autonomy.
|
|
102
|
+
|
|
103
|
+
| Field | Type | Req | Meaning |
|
|
104
|
+
|---|---|:--:|---|
|
|
105
|
+
| `id` | string | ✓ | Stable id (upsert key). |
|
|
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`.) |
|
|
107
|
+
| `status` | `"open"` \| `"resolved"` | | Default `"open"`. |
|
|
108
|
+
| `title` | string | ✓ | The ask, in one line. |
|
|
109
|
+
| `unit` | string | | The `Unit.id` this concerns. |
|
|
110
|
+
| `found` | string | | What the agent found / why it's stuck. |
|
|
111
|
+
| `need` | string | | What it needs from the human. |
|
|
112
|
+
| `options` | string[] | | Candidate resolutions. |
|
|
113
|
+
| `details` | `{ l, v }[]` | | Labelled facts (e.g. amount at risk). |
|
|
114
|
+
| `refs` | `{ label, artifact? }[]` | | Pointers to artifacts that back the ask. |
|
|
115
|
+
| `ts` | string (ISO-8601 w/ offset) | | |
|
|
116
|
+
|
|
117
|
+
> In v1, an ask is **one-way**: it announces that a human is needed. Resolution happens in the agent's own
|
|
118
|
+
> workflow (the agent sets `status: "resolved"` on a later push). Answers flowing *back* through Figs are
|
|
119
|
+
> [reserved for a future version](#reserved-not-in-v1).
|
|
120
|
+
|
|
121
|
+
## 7. `artifacts/` — rendered files
|
|
122
|
+
|
|
123
|
+
Files referenced by a run's `artifact` or an ask's `refs[].artifact`. Each is content-addressed (an
|
|
124
|
+
unchanged file is skipped on publish).
|
|
125
|
+
|
|
126
|
+
- **Supported kinds** (by extension): `html`, `markdown` (`.md`), `text` (`.txt`), `json`, and `image`
|
|
127
|
+
(`.png` `.jpg` `.gif` `.webp` `.svg`).
|
|
128
|
+
- **Size:** keep each file **≤ ~3 MB** (compress images client-side if needed).
|
|
129
|
+
- Artifacts are shown in a **sandboxed iframe** by the reader; an artifact cannot reach the host app.
|
|
130
|
+
|
|
131
|
+
## 8. Publishing (the wire contract)
|
|
132
|
+
|
|
133
|
+
`push` sends two things, authenticated by a per-user token in the `x-figs-token` header:
|
|
134
|
+
|
|
135
|
+
1. **The spine** → `POST {endpoint}/api/ingest`, body:
|
|
136
|
+
```jsonc
|
|
137
|
+
{
|
|
138
|
+
"workspaceId": "<uuid>", // from config.json
|
|
139
|
+
"agent": { /* agent.json */ },
|
|
140
|
+
"runs": [ /* runs.jsonl */ ], // optional
|
|
141
|
+
"asks": [ /* asks.jsonl */ ] // optional
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
2. **Each referenced artifact** → `POST {endpoint}/api/artifacts/upload`, content base64-encoded (so
|
|
145
|
+
binaries survive), hash server-verified.
|
|
146
|
+
|
|
147
|
+
The server upserts the agent by `id` and runs/asks by `id`; it never deletes. An agent **self-registers**
|
|
148
|
+
on first push — there is no "create agent" step.
|
|
149
|
+
|
|
150
|
+
## 9. Validation & versioning
|
|
151
|
+
|
|
152
|
+
- A `.figs/` folder can be validated against this contract before publishing (`figs doctor` →
|
|
153
|
+
`POST {endpoint}/api/validate`). The shapes are the source of truth; readers reject malformed payloads.
|
|
154
|
+
- **`figs-spec` is integer-versioned.** v1 is the current version. **Additive/optional** fields keep the
|
|
155
|
+
version number (an older `agent.json` still validates). The number is bumped only on a **breaking**
|
|
156
|
+
change. (Implementations report support via `GET {endpoint}/api/version`.)
|
|
157
|
+
- v1 is intentionally minimal — it defines the smallest useful surface so we don't freeze the wrong
|
|
158
|
+
abstractions early. Extensions arrive as additive optional fields until a breaking change is unavoidable.
|
|
159
|
+
|
|
160
|
+
## Reserved (not in v1)
|
|
161
|
+
|
|
162
|
+
Deliberately out of scope for v1, named here so implementers don't repurpose these concepts:
|
|
163
|
+
|
|
164
|
+
- **Two-way / answer-down.** A human answer or sign-off flowing *back* to the agent through Figs (vs. the
|
|
165
|
+
agent resolving in its own workflow). v1 is report-only.
|
|
166
|
+
- **Provenance / signing.** Cryptographic attestation that a report is complete, fresh, and untampered.
|
|
167
|
+
v1 state is *self-reported*; treat it as visibility, not a tamper-evident audit trail.
|
|
168
|
+
- **Per-record visibility / scoping.** v1 publishes to a workspace where all members can read everything.
|
|
169
|
+
|
|
170
|
+
## 10. A complete example
|
|
171
|
+
|
|
172
|
+
```jsonc
|
|
173
|
+
// .figs/config.json
|
|
174
|
+
{ "endpoint": "https://app.figs.so", "workspaceId": "…uuid…", "agentId": "…uuid…" }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```jsonc
|
|
178
|
+
// .figs/agent.json (no `id` here — `figs init` puts it in config.json; the CLI attaches it on push)
|
|
179
|
+
{
|
|
180
|
+
"name": "Reconciliation",
|
|
181
|
+
"type": "agent",
|
|
182
|
+
"role": "Reconciliation Officer",
|
|
183
|
+
"status": "in_dev",
|
|
184
|
+
"avatar": { "seed": "Reconciliation" },
|
|
185
|
+
"org": { "department": "Finance Ops" },
|
|
186
|
+
"runtime": "Claude Code",
|
|
187
|
+
"cadence": "Monthly",
|
|
188
|
+
"mandate": "Reconciles open invoices every month — flags what doesn't match for review.",
|
|
189
|
+
"steps": [
|
|
190
|
+
"Pull our open invoices and the customer's statement for the month.",
|
|
191
|
+
"Match on PO / delivery-number keys within tolerance.",
|
|
192
|
+
"Classify every key — matched / needs-review / our-side-only / customer-only — with a 'why'.",
|
|
193
|
+
"Surface discrepancies. Never write back to the source."
|
|
194
|
+
],
|
|
195
|
+
"properties": [
|
|
196
|
+
{ "k": "Data sources", "v": "Stripe · NetSuite" },
|
|
197
|
+
{ "k": "Escalation", "v": "#finance-ops" }
|
|
198
|
+
],
|
|
199
|
+
"units": [
|
|
200
|
+
{ "id": "acme", "name": "Acme Corp", "status": "88% matched · 31 keys flagged", "period": "2025-11",
|
|
201
|
+
"stats": [ { "l": "Matched", "v": "2,161 keys" }, { "l": "Needs review", "v": "31 keys" } ] }
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```jsonc
|
|
207
|
+
// .figs/runs.jsonl (one object per line)
|
|
208
|
+
{ "id": "acme-2025-11", "ts": "2026-05-28T23:41:26Z", "unit": "acme", "period": "2025-11", "result": "88% matched · 31 keys flagged", "status": "ok", "artifact": "acme-2025-11.html" }
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
```jsonc
|
|
212
|
+
// .figs/asks.jsonl (one object per line)
|
|
213
|
+
{ "id": "acme-bridge", "ts": "2026-05-28T21:05:00Z", "type": "needs-decision", "status": "open", "unit": "acme",
|
|
214
|
+
"title": "No bridge rule for prefixed invoice numbers",
|
|
215
|
+
"found": "~180 rows can't be matched safely; guessing risks false matches.",
|
|
216
|
+
"need": "Confirm the bridge rule for prefixed invoice numbers.",
|
|
217
|
+
"options": [ "Strip the alpha prefix", "Use a mapping you provide", "Treat as out-of-scope" ],
|
|
218
|
+
"details": [ { "l": "Amount at risk", "v": "$50.0M" } ],
|
|
219
|
+
"refs": [ { "label": "Acme report", "artifact": "acme-2025-11.html" } ] }
|
|
220
|
+
```
|
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,10 +35,16 @@ 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
|
-
|
|
41
|
-
|
|
40
|
+
import { spawn } from "node:child_process"
|
|
41
|
+
|
|
42
|
+
// Single source of truth for the version: package.json (shipped alongside this
|
|
43
|
+
// file in the published package). One edit keeps `figs version`, the floor
|
|
44
|
+
// check, and the npm package in lockstep.
|
|
45
|
+
const VERSION = JSON.parse(
|
|
46
|
+
readFileSync(new URL("./package.json", import.meta.url), "utf8"),
|
|
47
|
+
).version
|
|
42
48
|
// Going-forward default; override with FIGS_ENDPOINT or .figs/config.json endpoint
|
|
43
49
|
// (e.g. FIGS_ENDPOINT=http://localhost:3000 for local dev).
|
|
44
50
|
const DEFAULT_ENDPOINT = "https://app.figs.so"
|
|
@@ -67,7 +73,7 @@ const COMMANDS = {
|
|
|
67
73
|
flags: [],
|
|
68
74
|
desc: "log in — browser device-flow, or save a pasted token",
|
|
69
75
|
more: [
|
|
70
|
-
"no arg → device flow: a
|
|
76
|
+
"no arg → device flow: opens a browser for a human to approve (you never see the token).",
|
|
71
77
|
"<token> → save a token you already have to ~/.figs/credentials.json.",
|
|
72
78
|
],
|
|
73
79
|
eg: "figs login",
|
|
@@ -80,11 +86,13 @@ const COMMANDS = {
|
|
|
80
86
|
eg: "figs workspaces",
|
|
81
87
|
},
|
|
82
88
|
init: {
|
|
83
|
-
args: "--workspace <slug-or-id> [--endpoint <url>]",
|
|
89
|
+
args: "[--workspace <slug-or-id>] [--endpoint <url>]",
|
|
84
90
|
flags: ["--workspace", "--endpoint"],
|
|
85
|
-
desc: "
|
|
91
|
+
desc: "scaffold .figs/ here (identity + charter/contract/guide templates)",
|
|
86
92
|
more: [
|
|
87
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 lists your workspaces so you can re-run with the right one.",
|
|
95
|
+
"Never clobbers: an existing agent.json / CONTRACT.md / GUIDE.md / outbox is left exactly as-is.",
|
|
88
96
|
],
|
|
89
97
|
eg: "figs init --workspace acme-corp",
|
|
90
98
|
},
|
|
@@ -207,9 +215,9 @@ function cmpSemver(a, b) {
|
|
|
207
215
|
return 0
|
|
208
216
|
}
|
|
209
217
|
/**
|
|
210
|
-
* Cached (daily)
|
|
211
|
-
*
|
|
212
|
-
*
|
|
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).
|
|
213
221
|
*/
|
|
214
222
|
async function checkVersion({ force = false, hardFail = false } = {}) {
|
|
215
223
|
const cachePath = join(globalDir, "version-check.json")
|
|
@@ -228,14 +236,11 @@ async function checkVersion({ force = false, hardFail = false } = {}) {
|
|
|
228
236
|
}
|
|
229
237
|
}
|
|
230
238
|
const min = info?.cli?.min
|
|
231
|
-
const latest = info?.cli?.latest
|
|
232
239
|
// cmpSemver returns null on an unparseable version → skip (never fail closed).
|
|
233
240
|
if (min && cmpSemver(VERSION, min) === -1) {
|
|
234
241
|
const msg = `figs CLI ${VERSION} is below the minimum ${min} — upgrade: npx @figs-so/cli@latest`
|
|
235
242
|
if (hardFail) die(msg)
|
|
236
243
|
console.warn(`figs: ! ${msg}`)
|
|
237
|
-
} else if (latest && cmpSemver(VERSION, latest) === -1) {
|
|
238
|
-
console.warn(`figs: a newer CLI is available (${latest}) — npx @figs-so/cli@latest`)
|
|
239
244
|
}
|
|
240
245
|
}
|
|
241
246
|
|
|
@@ -306,9 +311,30 @@ function saveToken(token) {
|
|
|
306
311
|
}
|
|
307
312
|
}
|
|
308
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
|
+
|
|
309
334
|
/**
|
|
310
|
-
* `figs login` → device flow
|
|
311
|
-
*
|
|
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).
|
|
312
338
|
*/
|
|
313
339
|
async function login(token) {
|
|
314
340
|
if (token) {
|
|
@@ -320,9 +346,10 @@ async function login(token) {
|
|
|
320
346
|
const start = await request("POST", "/api/device/start")
|
|
321
347
|
if (!start.ok) die(`could not start login (${start.status})`)
|
|
322
348
|
const d = start.data
|
|
323
|
-
console.log("figs: to
|
|
324
|
-
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}`)
|
|
325
351
|
console.log(` (or go to ${d.verification_uri} and enter code: ${d.user_code})`)
|
|
352
|
+
openBrowser(d.verification_uri_complete)
|
|
326
353
|
console.log("figs: waiting for approval…")
|
|
327
354
|
|
|
328
355
|
const deadline = Date.now() + (d.expires_in ?? 600) * 1000
|
|
@@ -510,18 +537,91 @@ are a gitignored outbox. The token is the human's job — never generate one you
|
|
|
510
537
|
`
|
|
511
538
|
}
|
|
512
539
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
540
|
+
/**
|
|
541
|
+
* A starter `agent.json` — written by `figs init` only when none exists. The
|
|
542
|
+
* `<…>` values are placeholders the agent fills in by reading its own repo;
|
|
543
|
+
* `figs doctor` refuses to bless a charter that still has them. `name` defaults
|
|
544
|
+
* to the folder name (a sensible first guess), `id` is intentionally absent —
|
|
545
|
+
* the CLI attaches the identity UUID from config.json on push.
|
|
546
|
+
*/
|
|
547
|
+
function agentJsonStub(name) {
|
|
548
|
+
return (
|
|
549
|
+
JSON.stringify(
|
|
550
|
+
{
|
|
551
|
+
name,
|
|
552
|
+
role: "<one line — what you are>",
|
|
553
|
+
status: "in_dev",
|
|
554
|
+
mandate: "<one sentence — what you are accountable for>",
|
|
555
|
+
org: { department: "<your team / department>" },
|
|
556
|
+
runtime: "<what runs you, e.g. Claude Code>",
|
|
557
|
+
cadence: "<on-demand · weekly · monthly · …>",
|
|
558
|
+
responsibilities: ["<an area of work you own — list a few, or use steps>"],
|
|
559
|
+
properties: [{ k: "<fact>", v: "<value>" }],
|
|
560
|
+
},
|
|
561
|
+
null,
|
|
562
|
+
2,
|
|
563
|
+
) + "\n"
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** A starter activity contract — written by `figs init` only when none exists. */
|
|
568
|
+
function contractStub(name) {
|
|
569
|
+
return `# Activity contract — ${name} on Figs
|
|
570
|
+
|
|
571
|
+
What this agent surfaces to Figs vs. holds back. **Agree it with your user** — this is the
|
|
572
|
+
deliberate Activity step, not something to do mechanically. See \`.figs/GUIDE.md\` for the why.
|
|
573
|
+
|
|
574
|
+
> **Maintain:** edit when the surfacing agreement changes (a new stream, a sensitivity change).
|
|
575
|
+
> Keep it honest to what you actually push.
|
|
576
|
+
|
|
577
|
+
## What I surface
|
|
578
|
+
|
|
579
|
+
| Stream | Surface? | Content |
|
|
580
|
+
|--------|----------|---------|
|
|
581
|
+
| **runs** | <yes/no> | one line per run — what I did, de-identified scope, the headline result + status. |
|
|
582
|
+
| **artifacts** | <yes/no> | the report(s) a run produced. |
|
|
583
|
+
| **asks** | when real | genuine blockers / decisions / sign-offs / FYIs for my manager. 0 is a fine number. |
|
|
584
|
+
|
|
585
|
+
## What I never surface
|
|
586
|
+
|
|
587
|
+
Raw user content — ever. Plus, for this agent: <anything sensitive to its domain>. Use
|
|
588
|
+
**de-identified labels** (\`<scope>-01\`), never customer or system names.
|
|
589
|
+
`
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Find string values still left as `<…>` template placeholders, with their JSON
|
|
594
|
+
* path. Used by `figs doctor` to block publishing a half-filled charter. Matches
|
|
595
|
+
* a value that is *entirely* a placeholder (e.g. "<one line — what you are>") so
|
|
596
|
+
* real content containing stray angle brackets isn't flagged.
|
|
597
|
+
*/
|
|
598
|
+
function findPlaceholders(obj) {
|
|
599
|
+
const out = []
|
|
600
|
+
const walk = (v, path) => {
|
|
601
|
+
if (typeof v === "string") {
|
|
602
|
+
if (/^<.*>$/.test(v.trim())) out.push({ path: path || "(root)", value: v })
|
|
603
|
+
} else if (Array.isArray(v)) {
|
|
604
|
+
v.forEach((x, i) => walk(x, `${path}[${i}]`))
|
|
605
|
+
} else if (v && typeof v === "object") {
|
|
606
|
+
for (const [k, x] of Object.entries(v)) walk(x, path ? `${path}.${k}` : k)
|
|
607
|
+
}
|
|
517
608
|
}
|
|
518
|
-
|
|
609
|
+
walk(obj, "")
|
|
610
|
+
return out
|
|
611
|
+
}
|
|
519
612
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
613
|
+
/**
|
|
614
|
+
* Resolve which workspace this `.figs/` belongs to. `--workspace` is optional:
|
|
615
|
+
* - given a UUID → use it as-is (no network).
|
|
616
|
+
* - given a slug → resolve to its UUID via the API (needs auth).
|
|
617
|
+
* - omitted → reuse the one already in config.json (idempotent re-init),
|
|
618
|
+
* else list the user's workspaces and have them re-run with one
|
|
619
|
+
* (the agent drives the choice — we never silently pick).
|
|
620
|
+
* Returns the workspace UUID, or exits with an actionable message.
|
|
621
|
+
*/
|
|
622
|
+
async function resolveWorkspaceId(workspaceArg, endpoint) {
|
|
623
|
+
if (workspaceArg) {
|
|
624
|
+
if (isUuid(workspaceArg)) return workspaceArg
|
|
525
625
|
if (!getToken()) {
|
|
526
626
|
die("not logged in — run `figs login` first (resolving a workspace slug needs auth; or pass the workspace UUID)")
|
|
527
627
|
}
|
|
@@ -531,12 +631,36 @@ async function init() {
|
|
|
531
631
|
}
|
|
532
632
|
const list = r.data.workspaces ?? []
|
|
533
633
|
const match = list.find((w) => w.slug === workspaceArg || w.id === workspaceArg)
|
|
534
|
-
if (!match) {
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
workspaceId = match.id
|
|
634
|
+
if (!match) die(`no workspace matching "${workspaceArg}" — run \`figs workspaces\` to see yours`)
|
|
635
|
+
return match.id
|
|
538
636
|
}
|
|
539
637
|
|
|
638
|
+
// No --workspace: a re-init keeps the workspace already on file.
|
|
639
|
+
const existing = readJson(join(repoDir, "config.json"), null)
|
|
640
|
+
if (existing?.workspaceId) return existing.workspaceId
|
|
641
|
+
|
|
642
|
+
// First-time init with no workspace named — list them so the agent can re-run.
|
|
643
|
+
if (!getToken()) {
|
|
644
|
+
die("which workspace? run `figs login` first so I can list them, then `figs init --workspace <slug>` (or pass a workspace UUID directly)")
|
|
645
|
+
}
|
|
646
|
+
const r = await request("GET", "/api/workspaces", null, getToken())
|
|
647
|
+
if (!r.ok) die(`could not list workspaces (${r.status}): ${r.data.error ?? r.data.raw ?? ""}`)
|
|
648
|
+
const list = r.data.workspaces ?? []
|
|
649
|
+
if (list.length === 0) {
|
|
650
|
+
die(`no workspaces yet — create one at ${endpoint}, then re-run \`figs init --workspace <slug>\``)
|
|
651
|
+
}
|
|
652
|
+
console.log("figs: which workspace? re-run init with one of these:")
|
|
653
|
+
for (const w of list) console.log(` figs init --workspace ${w.slug} (${w.name})`)
|
|
654
|
+
process.exit(1)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async function init() {
|
|
658
|
+
const endpoint = (flag("--endpoint") || resolveEndpoint()).replace(/\/+$/, "")
|
|
659
|
+
const workspaceId = await resolveWorkspaceId(flag("--workspace"), endpoint)
|
|
660
|
+
|
|
661
|
+
// config.json (identity + destination) is always (re)written — it's the one
|
|
662
|
+
// file the CLI owns. A re-init reuses the existing identity UUID so every
|
|
663
|
+
// runner of this repo pushes to the same agent.
|
|
540
664
|
const existing = readJson(join(repoDir, "config.json"), null)
|
|
541
665
|
const agentId = existing?.agentId || randomUUID()
|
|
542
666
|
mkdirSync(repoDir, { recursive: true })
|
|
@@ -544,48 +668,87 @@ async function init() {
|
|
|
544
668
|
join(repoDir, "config.json"),
|
|
545
669
|
JSON.stringify({ endpoint, workspaceId, agentId }, null, 2) + "\n",
|
|
546
670
|
)
|
|
547
|
-
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
"artifacts/",
|
|
559
|
-
"credentials.json",
|
|
560
|
-
"",
|
|
561
|
-
].join("\n"),
|
|
562
|
-
)
|
|
671
|
+
|
|
672
|
+
// Everything else is scaffolded create-if-missing — `figs init` gives a fresh
|
|
673
|
+
// repo a complete, ready-to-fill `.figs/`, and never clobbers an agent's
|
|
674
|
+
// authored charter/contract/guide or its activity outbox.
|
|
675
|
+
const created = []
|
|
676
|
+
const ensure = (rel, contents) => {
|
|
677
|
+
const p = join(repoDir, rel)
|
|
678
|
+
if (existsSync(p)) return false
|
|
679
|
+
writeFileSync(p, contents)
|
|
680
|
+
created.push(rel)
|
|
681
|
+
return true
|
|
563
682
|
}
|
|
564
|
-
|
|
683
|
+
ensure(
|
|
684
|
+
".gitignore",
|
|
685
|
+
[
|
|
686
|
+
"# Figs — commit config.json + agent.json + CONTRACT.md + GUIDE.md.",
|
|
687
|
+
"# Activity is a transient outbox: emitted per run, aggregated remotely.",
|
|
688
|
+
"runs.jsonl",
|
|
689
|
+
"asks.jsonl",
|
|
690
|
+
"artifacts/",
|
|
691
|
+
"credentials.json",
|
|
692
|
+
"",
|
|
693
|
+
].join("\n"),
|
|
694
|
+
)
|
|
695
|
+
ensure("GUIDE.md", guideStub(endpoint))
|
|
696
|
+
const name = basename(process.cwd())
|
|
697
|
+
const charterCreated = ensure("agent.json", agentJsonStub(name))
|
|
698
|
+
ensure("CONTRACT.md", contractStub(name))
|
|
699
|
+
ensure("runs.jsonl", "")
|
|
700
|
+
ensure("asks.jsonl", "")
|
|
701
|
+
mkdirSync(join(repoDir, "artifacts"), { recursive: true })
|
|
565
702
|
|
|
566
703
|
console.log(
|
|
567
|
-
`figs: ✓ .figs/
|
|
704
|
+
`figs: ✓ .figs/ ready — config.json written (agentId ${agentId}, workspace ${workspaceId})`,
|
|
568
705
|
)
|
|
706
|
+
if (created.length) console.log(` scaffolded: ${created.join(", ")}`)
|
|
707
|
+
if (charterCreated) {
|
|
708
|
+
console.log(
|
|
709
|
+
" Phase 1: fill in .figs/agent.json — it's a template; replace the <…> placeholders",
|
|
710
|
+
)
|
|
711
|
+
console.log(
|
|
712
|
+
" (`figs doctor` flags any you miss), then `figs doctor` && `figs push` to appear.",
|
|
713
|
+
)
|
|
714
|
+
} else {
|
|
715
|
+
console.log(
|
|
716
|
+
" Your charter (.figs/agent.json) is already here — `figs doctor` && `figs push` to publish.",
|
|
717
|
+
)
|
|
718
|
+
}
|
|
569
719
|
console.log(
|
|
570
|
-
|
|
720
|
+
" Anchor Figs in the file you load every session (CLAUDE.md/AGENTS.md/…): paste the",
|
|
571
721
|
)
|
|
722
|
+
console.log(` figs:begin block from ${endpoint}/llms.txt, or future sessions forget Figs.`)
|
|
572
723
|
console.log(
|
|
573
|
-
|
|
724
|
+
" Commit config.json + agent.json + CONTRACT.md + GUIDE.md; never commit credentials.json.",
|
|
574
725
|
)
|
|
575
726
|
console.log(` Full guide: ${endpoint}/llms.txt`)
|
|
576
727
|
}
|
|
577
728
|
|
|
578
729
|
/** Validate the local .figs/ payload against the contract — no write. */
|
|
579
730
|
async function doctor() {
|
|
580
|
-
|
|
581
|
-
if (!existsSync(repoDir)) die("no .figs/ here — run `figs init
|
|
731
|
+
// Local checks first (no token/network needed) — fail fast and offline.
|
|
732
|
+
if (!existsSync(repoDir)) die("no .figs/ here — run `figs init` first")
|
|
582
733
|
const config = readJson(join(repoDir, "config.json"), {})
|
|
583
734
|
if (!config.workspaceId || !config.agentId) {
|
|
584
|
-
die("config missing workspaceId/agentId — run `figs init
|
|
735
|
+
die("config missing workspaceId/agentId — run `figs init`")
|
|
585
736
|
}
|
|
586
737
|
const agentJson = readJson(join(repoDir, "agent.json"), null)
|
|
587
738
|
if (!agentJson) die("missing .figs/agent.json — author it first (see .figs/GUIDE.md)")
|
|
588
739
|
|
|
740
|
+
// Refuse to bless a charter that still has `<…>` template placeholders — `figs
|
|
741
|
+
// init` scaffolds them, and pushing them would publish "<one line — what you
|
|
742
|
+
// are>" to the org chart. This is the "not ready to push" signal.
|
|
743
|
+
const placeholders = findPlaceholders(agentJson)
|
|
744
|
+
if (placeholders.length) {
|
|
745
|
+
console.log("figs: ✗ .figs/agent.json still has template placeholders — fill these in before pushing:")
|
|
746
|
+
for (const p of placeholders) console.log(` ${p.path}: ${p.value}`)
|
|
747
|
+
console.log(" (replace the <…> values by reading your own repo, then re-run `figs doctor`)")
|
|
748
|
+
process.exit(1)
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (!getToken()) die("not logged in — run `figs login`")
|
|
589
752
|
const r = await api("POST", "/api/validate", {
|
|
590
753
|
workspaceId: config.workspaceId,
|
|
591
754
|
agent: { ...agentJson, id: config.agentId },
|
|
@@ -616,11 +779,11 @@ async function push() {
|
|
|
616
779
|
if (!token) die("not logged in — run `figs login` (or set FIGS_TOKEN)")
|
|
617
780
|
await checkVersion({ hardFail: true })
|
|
618
781
|
if (!existsSync(repoDir)) {
|
|
619
|
-
die("no .figs/ here — run `figs init
|
|
782
|
+
die("no .figs/ here — run `figs init` first")
|
|
620
783
|
}
|
|
621
784
|
const config = readJson(join(repoDir, "config.json"), {})
|
|
622
785
|
if (!config.workspaceId || !config.agentId) {
|
|
623
|
-
die("config missing workspaceId/agentId — run `figs init
|
|
786
|
+
die("config missing workspaceId/agentId — run `figs init`")
|
|
624
787
|
}
|
|
625
788
|
const endpoint =
|
|
626
789
|
process.env.FIGS_ENDPOINT || config.endpoint || DEFAULT_ENDPOINT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@figs-so/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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": {
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"figs.mjs",
|
|
11
|
-
"README.md"
|
|
11
|
+
"README.md",
|
|
12
|
+
"SPEC.md",
|
|
13
|
+
"LICENSE"
|
|
12
14
|
],
|
|
13
15
|
"engines": {
|
|
14
16
|
"node": ">=18"
|
|
@@ -21,6 +23,13 @@
|
|
|
21
23
|
],
|
|
22
24
|
"license": "MIT",
|
|
23
25
|
"homepage": "https://figs.so",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/figs-so/figs.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/figs-so/figs/issues"
|
|
32
|
+
},
|
|
24
33
|
"publishConfig": {
|
|
25
34
|
"access": "public"
|
|
26
35
|
}
|