@figs-so/cli 0.1.3 → 0.1.6
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 +1 -0
- package/figs.mjs +86 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,7 @@ The full, always-current guide + `agent.json` schema is served at **`<endpoint>/
|
|
|
25
25
|
|---|---|
|
|
26
26
|
| `figs status [--json]` | login / workspace / agent state |
|
|
27
27
|
| `figs login` | device-flow browser approve (or `figs login <token>`) |
|
|
28
|
+
| `figs logout` | remove the locally-saved token (`~/.figs/credentials.json`) |
|
|
28
29
|
| `figs workspaces [--json]` | list your workspaces (read-only; create one in the web app) |
|
|
29
30
|
| `figs init --workspace <slug-or-id>` | resolve the workspace, generate identity UUID + config + pointer GUIDE.md |
|
|
30
31
|
| `figs doctor` | validate `.figs/` against the contract |
|
package/figs.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* figs status show login / workspace / agent state [--json]
|
|
6
6
|
* figs login browser approve (device flow) — agent never sees the token
|
|
7
7
|
* figs login <token> fallback: save a token you pasted (~/.figs/credentials.json)
|
|
8
|
+
* figs logout remove the locally-saved token (~/.figs/credentials.json)
|
|
8
9
|
* figs workspaces list the user's workspaces [--json]
|
|
9
10
|
* figs init --workspace <slug-or-id> [--endpoint <url>]
|
|
10
11
|
* create .figs/config.json + GUIDE.md (generates a stable agent id)
|
|
@@ -25,13 +26,14 @@ import {
|
|
|
25
26
|
existsSync,
|
|
26
27
|
mkdirSync,
|
|
27
28
|
readFileSync,
|
|
29
|
+
rmSync,
|
|
28
30
|
writeFileSync,
|
|
29
31
|
} from "node:fs"
|
|
30
32
|
import { homedir } from "node:os"
|
|
31
33
|
import { join } from "node:path"
|
|
32
34
|
import { randomUUID } from "node:crypto"
|
|
33
35
|
|
|
34
|
-
const VERSION = "0.1.
|
|
36
|
+
const VERSION = "0.1.6"
|
|
35
37
|
// Going-forward default; override with FIGS_ENDPOINT or .figs/config.json endpoint
|
|
36
38
|
// (e.g. FIGS_ENDPOINT=http://localhost:3000 for local dev).
|
|
37
39
|
const DEFAULT_ENDPOINT = "https://app.figs.so"
|
|
@@ -143,6 +145,7 @@ async function checkVersion({ force = false, hardFail = false } = {}) {
|
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
if (cmd === "login") await login(process.argv[3])
|
|
148
|
+
else if (cmd === "logout") logout()
|
|
146
149
|
else if (cmd === "status") await status()
|
|
147
150
|
else if (cmd === "workspaces") await workspaces()
|
|
148
151
|
else if (cmd === "init") await init()
|
|
@@ -153,7 +156,7 @@ else if (cmd === "version" || cmd === "--version") {
|
|
|
153
156
|
await checkVersion({ force: true })
|
|
154
157
|
} else {
|
|
155
158
|
die(
|
|
156
|
-
`unknown command "${cmd}" — try: status, login, workspaces, init, doctor, push`,
|
|
159
|
+
`unknown command "${cmd}" — try: status, login, logout, workspaces, init, doctor, push`,
|
|
157
160
|
)
|
|
158
161
|
}
|
|
159
162
|
|
|
@@ -197,16 +200,48 @@ async function login(token) {
|
|
|
197
200
|
}
|
|
198
201
|
if (status === "denied") die("authorization denied")
|
|
199
202
|
if (status === "expired") die("code expired — run `figs login` again")
|
|
203
|
+
if (status === "already_claimed") {
|
|
204
|
+
die("this login was already completed (on another run) — run `figs status` to check; re-run `figs login` if no token was saved")
|
|
205
|
+
}
|
|
206
|
+
if (status === "not_found") die("login code not found — run `figs login` again")
|
|
200
207
|
// pending → keep polling
|
|
201
208
|
}
|
|
202
209
|
die("timed out waiting for approval — run `figs login` again")
|
|
203
210
|
}
|
|
204
211
|
|
|
212
|
+
/**
|
|
213
|
+
* `figs logout` — remove the locally-saved token (`~/.figs/credentials.json`).
|
|
214
|
+
* This only clears *this machine's* copy; the token still exists server-side
|
|
215
|
+
* until you revoke it in Settings. A token supplied via the FIGS_TOKEN env var
|
|
216
|
+
* can't be removed here (unset the env var to fully log out).
|
|
217
|
+
*/
|
|
218
|
+
function logout() {
|
|
219
|
+
if (existsSync(globalCreds)) {
|
|
220
|
+
try {
|
|
221
|
+
rmSync(globalCreds)
|
|
222
|
+
} catch (e) {
|
|
223
|
+
die(`could not remove ${globalCreds}: ${e?.message || e}`)
|
|
224
|
+
}
|
|
225
|
+
console.log("figs: ✓ logged out — removed ~/.figs/credentials.json")
|
|
226
|
+
} else {
|
|
227
|
+
console.log("figs: not logged in — no ~/.figs/credentials.json to remove")
|
|
228
|
+
}
|
|
229
|
+
if (process.env.FIGS_TOKEN) {
|
|
230
|
+
console.warn(
|
|
231
|
+
"figs: ! FIGS_TOKEN is still set in your environment — `unset FIGS_TOKEN` to fully log out",
|
|
232
|
+
)
|
|
233
|
+
}
|
|
234
|
+
console.log(
|
|
235
|
+
` the token still exists server-side — revoke it at ${resolveEndpoint()}/settings`,
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
|
|
205
239
|
/** Show where setup stands — login, workspace, charter. Drives the agent's next step. */
|
|
206
240
|
async function status() {
|
|
207
241
|
const token = getToken()
|
|
208
242
|
const cfg = readJson(join(repoDir, "config.json"), null)
|
|
209
243
|
const hasAgent = existsSync(join(repoDir, "agent.json"))
|
|
244
|
+
const hasContract = existsSync(join(repoDir, "CONTRACT.md"))
|
|
210
245
|
const endpoint = resolveEndpoint()
|
|
211
246
|
|
|
212
247
|
let loggedIn = false
|
|
@@ -229,6 +264,7 @@ async function status() {
|
|
|
229
264
|
workspaces: list?.map((w) => ({ id: w.id, name: w.name, role: w.role })),
|
|
230
265
|
config: cfg ? { workspaceId: cfg.workspaceId, agentId: cfg.agentId } : null,
|
|
231
266
|
agentJson: hasAgent,
|
|
267
|
+
contractMd: hasContract,
|
|
232
268
|
},
|
|
233
269
|
null,
|
|
234
270
|
2,
|
|
@@ -255,7 +291,13 @@ async function status() {
|
|
|
255
291
|
? cfg.workspaceId
|
|
256
292
|
: "not initialized — run `figs init --workspace <id>`",
|
|
257
293
|
)
|
|
258
|
-
row("agent.json", hasAgent ? "present" : "missing — author .figs/agent.json")
|
|
294
|
+
row("agent.json", hasAgent ? "present (identity)" : "missing — author .figs/agent.json")
|
|
295
|
+
row(
|
|
296
|
+
"contract",
|
|
297
|
+
hasContract
|
|
298
|
+
? "present (activity) — follow it"
|
|
299
|
+
: "none yet — Activity is optional, agree it with your user",
|
|
300
|
+
)
|
|
259
301
|
row("endpoint", endpoint)
|
|
260
302
|
row("cli", VERSION)
|
|
261
303
|
}
|
|
@@ -286,31 +328,30 @@ function isUuid(s) {
|
|
|
286
328
|
}
|
|
287
329
|
|
|
288
330
|
/**
|
|
289
|
-
* The local `.figs/GUIDE.md` is a thin
|
|
290
|
-
* always-current guide +
|
|
291
|
-
*
|
|
331
|
+
* The local `.figs/GUIDE.md` is a thin orientation breadcrumb (safe to
|
|
332
|
+
* regenerate) — the full, always-current guide + schema is served at
|
|
333
|
+
* `${endpoint}/llms.txt`, and the agent's own standing agreement lives in the
|
|
334
|
+
* precious, agent-authored `.figs/CONTRACT.md` (which `init` never touches).
|
|
292
335
|
*/
|
|
293
336
|
function guideStub(endpoint) {
|
|
294
|
-
return `<!-- Written by \`figs init
|
|
295
|
-
|
|
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.
|
|
337
|
+
return `<!-- Written by \`figs init\` (safe to regenerate). Orientation only — the full
|
|
338
|
+
guide lives at the URL below; your own contract lives in CONTRACT.md. -->
|
|
339
|
+
# Figs — this folder
|
|
300
340
|
|
|
301
|
-
|
|
341
|
+
This repo publishes to **Figs** (your manager's read-only window into what you do).
|
|
302
342
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
343
|
+
- **Full guide + schema → ${endpoint}/llms.txt** — read this first.
|
|
344
|
+
- **Your contract → ./CONTRACT.md** — what you publish / hold back (you + your user author it).
|
|
345
|
+
- **Identity → config.json** (committed) · **charter → agent.json** (you write).
|
|
308
346
|
|
|
309
|
-
|
|
310
|
-
|
|
347
|
+
**Where are you?** You may be reading this at any time — don't assume you're starting fresh. Run
|
|
348
|
+
\`figs status\` and check \`.figs/\` before acting:
|
|
349
|
+
• not set up yet → author \`.figs/agent.json\`, then \`figs doctor\` && \`figs push\` to appear (Identity).
|
|
350
|
+
• CONTRACT.md present → follow it; keep publishing what it says.
|
|
351
|
+
• no CONTRACT.md yet → agree with your user what work to surface (Activity) — see ${endpoint}/llms.txt.
|
|
311
352
|
|
|
312
|
-
Commit config.json + agent.json
|
|
313
|
-
|
|
353
|
+
Commit config.json + agent.json + CONTRACT.md + this file. runs.jsonl / asks.jsonl / artifacts/
|
|
354
|
+
are a gitignored outbox. The token is the human's job — never generate one yourself.
|
|
314
355
|
`
|
|
315
356
|
}
|
|
316
357
|
|
|
@@ -355,7 +396,7 @@ async function init() {
|
|
|
355
396
|
writeFileSync(
|
|
356
397
|
giPath,
|
|
357
398
|
[
|
|
358
|
-
"# Figs — commit config.json + agent.json
|
|
399
|
+
"# Figs — commit config.json + agent.json + CONTRACT.md + GUIDE.md.",
|
|
359
400
|
"# Activity is a transient outbox: emitted per run, aggregated remotely.",
|
|
360
401
|
"runs.jsonl",
|
|
361
402
|
"asks.jsonl",
|
|
@@ -371,8 +412,9 @@ async function init() {
|
|
|
371
412
|
`figs: ✓ .figs/config.json + .gitignore + GUIDE.md written (agentId ${agentId})`,
|
|
372
413
|
)
|
|
373
414
|
console.log(
|
|
374
|
-
`
|
|
415
|
+
` Phase 1: author .figs/agent.json (your charter), then \`figs doctor\` && \`figs push\` to appear.`,
|
|
375
416
|
)
|
|
417
|
+
console.log(` Full guide: ${endpoint}/llms.txt`)
|
|
376
418
|
}
|
|
377
419
|
|
|
378
420
|
/** Validate the local .figs/ payload against the contract — no write. */
|
|
@@ -445,8 +487,10 @@ async function push() {
|
|
|
445
487
|
* The spine ingest is JSON-only; artifacts go to a separate endpoint that stores
|
|
446
488
|
* them content-addressed (an unchanged file is skipped server-side). Content is
|
|
447
489
|
* sent base64-encoded so any type — html, markdown, text, json, images — survives.
|
|
448
|
-
*
|
|
449
|
-
*
|
|
490
|
+
* A **server rejection** (auth/size/etc.) is fatal: it prints ✗ and exits non-zero
|
|
491
|
+
* so the agent never believes a report published when it didn't (esp. the ~3 MB
|
|
492
|
+
* cap → 413). A **missing local file** is only a warning — that's the agent
|
|
493
|
+
* referencing an artifact it didn't actually produce, not a publish failure.
|
|
450
494
|
*/
|
|
451
495
|
async function pushArtifacts(base, token, config, runs, asks) {
|
|
452
496
|
const refNames = (asks ?? []).flatMap((a) =>
|
|
@@ -459,10 +503,13 @@ async function pushArtifacts(base, token, config, runs, asks) {
|
|
|
459
503
|
|
|
460
504
|
let uploaded = 0
|
|
461
505
|
let unchanged = 0
|
|
506
|
+
let missing = 0
|
|
507
|
+
let failed = 0
|
|
462
508
|
for (const name of names) {
|
|
463
509
|
const p = join(repoDir, "artifacts", name)
|
|
464
510
|
if (!existsSync(p)) {
|
|
465
511
|
console.warn(`figs: ! artifact missing, skipped: artifacts/${name}`)
|
|
512
|
+
missing++
|
|
466
513
|
continue
|
|
467
514
|
}
|
|
468
515
|
const content = readFileSync(p).toString("base64")
|
|
@@ -477,8 +524,13 @@ async function pushArtifacts(base, token, config, runs, asks) {
|
|
|
477
524
|
}),
|
|
478
525
|
})
|
|
479
526
|
if (!res.ok) {
|
|
480
|
-
const t = await res.text()
|
|
481
|
-
|
|
527
|
+
const t = await res.text().catch(() => "")
|
|
528
|
+
const hint =
|
|
529
|
+
res.status === 413 ? " — too large (>3 MB); compress or split it" : ""
|
|
530
|
+
console.error(
|
|
531
|
+
`figs: ✗ artifact upload failed (${res.status}) ${name}${hint}${t ? `: ${t}` : ""}`,
|
|
532
|
+
)
|
|
533
|
+
failed++
|
|
482
534
|
continue
|
|
483
535
|
}
|
|
484
536
|
const body = await res.json().catch(() => ({}))
|
|
@@ -486,8 +538,13 @@ async function pushArtifacts(base, token, config, runs, asks) {
|
|
|
486
538
|
else uploaded++
|
|
487
539
|
}
|
|
488
540
|
console.log(
|
|
489
|
-
`figs: ✓ artifacts — ${uploaded} uploaded, ${unchanged} unchanged
|
|
541
|
+
`figs: ${failed ? "✗" : "✓"} artifacts — ${uploaded} uploaded, ${unchanged} unchanged` +
|
|
542
|
+
(missing ? `, ${missing} missing` : "") +
|
|
543
|
+
(failed ? `, ${failed} failed` : ""),
|
|
490
544
|
)
|
|
545
|
+
// The spine already landed; signal a non-zero exit so an agent's run loop can
|
|
546
|
+
// catch that an artifact the manager needs to read did not publish.
|
|
547
|
+
if (failed) process.exit(1)
|
|
491
548
|
}
|
|
492
549
|
|
|
493
550
|
function readJsonl(name) {
|