@figs-so/cli 1.0.0 → 1.1.0

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 (3) hide show
  1. package/SPEC.md +8 -2
  2. package/figs.mjs +27 -3
  3. package/package.json +1 -1
package/SPEC.md CHANGED
@@ -259,7 +259,8 @@ minimum CLI version requires Bearer.)
259
259
  "agent": { /* agent.json */ },
260
260
  "runs": [ /* runs.jsonl */ ], // optional
261
261
  "asks": [ /* asks.jsonl */ ], // optional
262
- "messages": [ /* messages.jsonl */ ] // optional — transcribed replies the reader lacks
262
+ "messages": [ /* messages.jsonl */ ], // optional — transcribed replies the reader lacks
263
+ "confirmRename": true // optional — `figs push --rename`: confirm a real name change
263
264
  }
264
265
  ```
265
266
  2. **Each attached file** → `POST {endpoint}/api/artifacts/upload`, base64-encoded, hash-verified.
@@ -270,7 +271,12 @@ server refuses a fold older than the record's stored close/settle (a stale machi
270
271
  and accepts a newer one (a legitimate reopen — the `warn` → `ok` evolution). **A push never re-homes an
271
272
  agent:** a `workspaceId` differing from the agent's registered home is rejected `409`
272
273
  `{ "error", "code": "agent_moved", "workspaceId"? }`; the agent recovers by setting
273
- `config.json#workspaceId` to the named workspace and pushing again.
274
+ `config.json#workspaceId` to the named workspace and pushing again. **A push never silently
275
+ re-identifies an agent:** a `name` differing from the one registered for that `agentId` is the
276
+ fingerprint of a copied folder and is rejected `409` `{ "error", "code": "agent_renamed" }` — the
277
+ agent rotates identity (`rm -rf .figs && figs init`) for a genuinely new agent, or sets
278
+ `confirmRename` (`figs push --rename`) once to confirm a real rename. `name` is the signal because a
279
+ copy-to-make-another always renames while role/mandate evolve legitimately; the check is `name` only.
274
280
 
275
281
  **Down — the reply sync.** Delivery is **agent-pulled**, never pushed into the repo: a reader exposes a
276
282
  read returning **this agent's human messages** (answers/verdicts), which the CLI merges into
package/figs.mjs CHANGED
@@ -279,12 +279,16 @@ const COMMANDS = {
279
279
  },
280
280
  push: {
281
281
  args: "",
282
- flags: [],
282
+ flags: ["--rename"],
283
283
  desc: "publish .figs/ — spine to /api/ingest, artifacts to /api/artifacts",
284
284
  more: [
285
285
  "Idempotent (records fold by id). Exits non-zero if an artifact upload is rejected.",
286
286
  "The writing verbs (report/ask/resolve) call this automatically — you only need it",
287
287
  "after hand-editing files, after --no-push, or to retry a failed auto-push.",
288
+ "--rename: confirm a genuine name change on an already-registered agent (one",
289
+ "time). The server refuses a name that doesn't match the registered one — it's",
290
+ "the fingerprint of a copied folder; if that's what happened, rotate identity",
291
+ "instead with `rm -rf .figs && figs init`, don't --rename.",
288
292
  ],
289
293
  eg: "figs push",
290
294
  },
@@ -2135,6 +2139,9 @@ async function doPush() {
2135
2139
  const asks = foldById(readJsonl("asks.jsonl"))
2136
2140
  // Messages are immutable events — sent whole (no fold); the server dedupes by id.
2137
2141
  const messages = readJsonl("messages.jsonl")
2142
+ // One-time confirm that a name change on an already-registered id is a real
2143
+ // rename, not a copied folder (the server's rename guard refuses it otherwise).
2144
+ const confirmRename = hasFlag("--rename")
2138
2145
 
2139
2146
  // Local pre-flight — fail fast, offline, with teaching errors.
2140
2147
  const placeholders = findPlaceholders(agentJson)
@@ -2152,7 +2159,14 @@ async function doPush() {
2152
2159
  res = await fetchT(`${base}/api/ingest`, {
2153
2160
  method: "POST",
2154
2161
  headers: { "content-type": "application/json", ...authHeaders(token) },
2155
- body: JSON.stringify({ workspaceId: config.workspaceId, agent, runs, asks, messages }),
2162
+ body: JSON.stringify({
2163
+ workspaceId: config.workspaceId,
2164
+ agent,
2165
+ runs,
2166
+ asks,
2167
+ messages,
2168
+ ...(confirmRename ? { confirmRename: true } : {}),
2169
+ }),
2156
2170
  })
2157
2171
  } catch (e) {
2158
2172
  // Network/timeout — transient; the records are safe locally.
@@ -2160,7 +2174,17 @@ async function doPush() {
2160
2174
  }
2161
2175
  const text = await res.text()
2162
2176
  // 4xx = the payload is wrong (re-pushing won't help) → structural; 5xx = transient.
2163
- if (!res.ok) return fail(`server rejected it (${res.status}): ${text}`, res.status >= 500)
2177
+ // The server's teaching message rides in { error } surface that, not raw JSON.
2178
+ if (!res.ok) {
2179
+ let detail = text
2180
+ try {
2181
+ const body = JSON.parse(text)
2182
+ if (body?.error) detail = body.error
2183
+ } catch {
2184
+ // non-JSON body — keep the raw text
2185
+ }
2186
+ return fail(`server rejected it (${res.status}): ${detail}`, res.status >= 500)
2187
+ }
2164
2188
  console.log(
2165
2189
  `figs: ✓ pushed ${agent.name ?? agent.id} — ${runs.length} runs, ${asks.length} asks, ${messages.length} messages`,
2166
2190
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@figs-so/cli",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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": {