@lambdacurry/arbor 0.6.6 → 0.7.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 (2) hide show
  1. package/dist/arbor.js +113 -26
  2. package/package.json +1 -1
package/dist/arbor.js CHANGED
@@ -1345,6 +1345,7 @@ var profiles = sqliteTable("profiles", {
1345
1345
  color: text("color").$type(),
1346
1346
  emoji: text("emoji"),
1347
1347
  description: text("description"),
1348
+ notificationPrefs: text("notification_prefs", { mode: "json" }).$type(),
1348
1349
  createdAt: ts("created_at").notNull()
1349
1350
  }, (t) => ({ orgIdx: index("profiles_org_idx").on(t.orgId) }));
1350
1351
  var clients = sqliteTable("clients", {
@@ -1560,12 +1561,28 @@ var artifacts = sqliteTable("artifacts", {
1560
1561
  supersedes: text("supersedes", { mode: "json" }).$type(),
1561
1562
  supersededBy: text("superseded_by", { mode: "json" }).$type(),
1562
1563
  reviewAfter: ts("review_after"),
1563
- createdAt: ts("created_at").notNull()
1564
+ createdAt: ts("created_at").notNull(),
1565
+ kind: text("kind").$type().notNull().default("file"),
1566
+ source: text("source"),
1567
+ version: integer("version").notNull().default(0),
1568
+ editPolicy: text("edit_policy").$type().notNull().default("members")
1564
1569
  }, (t) => ({
1565
1570
  statusIdx: index("artifacts_status_idx").on(t.status),
1566
1571
  tierIdx: index("artifacts_tier_idx").on(t.retrievalTier),
1567
1572
  ownerIdx: index("artifacts_owner_idx").on(t.ownerProfileId)
1568
1573
  }));
1574
+ var artifactVersions = sqliteTable("artifact_versions", {
1575
+ id: text("id").primaryKey(),
1576
+ artifactId: text("artifact_id").notNull().references(() => artifacts.id),
1577
+ version: integer("version").notNull(),
1578
+ source: text("source").notNull(),
1579
+ editorProfileId: text("editor_profile_id").notNull().references(() => profiles.id),
1580
+ executionContextId: text("execution_context_id").references(() => executionContexts.id),
1581
+ note: text("note"),
1582
+ createdAt: ts("created_at").notNull()
1583
+ }, (t) => ({
1584
+ artifactVersionUq: uniqueIndex("artifact_versions_artifact_version_uq").on(t.artifactId, t.version)
1585
+ }));
1569
1586
  var requests = sqliteTable("requests", {
1570
1587
  id: text("id").primaryKey(),
1571
1588
  threadId: text("thread_id").notNull().references(() => threads.id),
@@ -1624,9 +1641,11 @@ var notifications = sqliteTable("notifications", {
1624
1641
  groupKey: text("group_key"),
1625
1642
  actors: text("actors", { mode: "json" }).$type().notNull().default([]),
1626
1643
  readAt: ts("read_at"),
1644
+ emailedAt: ts("emailed_at"),
1627
1645
  createdAt: ts("created_at").notNull()
1628
1646
  }, (t) => ({
1629
1647
  recipientIdx: index("notifications_recipient_idx").on(t.recipientProfileId, t.readAt),
1648
+ emailIdx: index("notifications_emailed_idx").on(t.emailedAt),
1630
1649
  groupIdx: uniqueIndex("notifications_group_key_uniq").on(t.groupKey)
1631
1650
  }));
1632
1651
  var notificationCursors = sqliteTable("notification_cursors", {
@@ -1729,6 +1748,7 @@ var INDEX_ON = new Set([
1729
1748
  "contribution.added",
1730
1749
  "contribution.edited",
1731
1750
  "artifact.created",
1751
+ "artifact.edited",
1732
1752
  "review.added",
1733
1753
  "thread.created",
1734
1754
  "topic.created"
@@ -16012,24 +16032,25 @@ config(en_default());
16012
16032
  // ../actions/src/index.ts
16013
16033
  var ORIENTATION = `Arbor is your team's deliberation room and shared memory — people and agents settle typed work here, and Arbor remembers what's decided. To work well:
16014
16034
 
16015
- 1. RECALL FIRST. Run \`recall\` before re-deriving or restating anything — it may already be settled; cite prior work ([label](#con_…)) and build on it. Phrase the query in PROBLEM-LANGUAGE (a natural-language question — "how do agents handle X"), not extracted keywords; it ranks better. Empty recall is itself worth noting. WHY: a room where everyone re-derives is just a chat log.
16016
- 2. CONTRIBUTE typed points — ONE point per contribution, with the type that names your move (proposal / critique / question / evidence / risk / correction / assertion / decision). Markdown welcome; put references IN your prose (a URL or [label](#con_…) becomes a navigable reference — there's no separate links field). WHY: one typed point is reviewable on its own.
16035
+ 1. RECALL FIRST — but Arbor is MEMORY, NOT TRUTH. Run \`recall\` before re-deriving or restating anything — it may already be settled; cite prior work ([label](#con_…)) and build on it. Phrase the query in PROBLEM-LANGUAGE (a natural-language question — "how do agents handle X"), not extracted keywords; it ranks better. Empty recall is itself worth noting. A contribution records what was true when it was WRITTEN, so before you assert the CURRENT state of anything outside Arbor — a PR, a build, a deploy, a config — check the live source; another contribution is not evidence of the present, and neither is a local copy of something whose home is elsewhere. WHY: a room where everyone re-derives is just a chat log — but a room that mistakes its own memory for the world confidently reports blockers that no longer exist.
16036
+ 2. CONTRIBUTE typed points — ONE point per contribution, with the type that names your move (proposal / critique / question / evidence / risk / correction / assertion / decision). Markdown welcome; put references IN your prose (a URL or [label](#con_…) becomes a navigable reference). Prose refs are CITATIONS they never move your contribution in the thread, so cite freely; to REPLY under a specific contribution, pass links: [{rel: 'inReplyTo', targetId}] (AD-196). WHY: one typed point is reviewable on its own, and a synthesis citing five points should read as the most top-level thing in the thread, not as a reply to the first one it mentions.
16017
16037
  3. ANSWER through requests. When \`inbox\` or a thread shows an open request you can meet, answer THROUGH it — \`respond\` to it, or \`stamp\` the contribution a review request is about — so it completes and the requester is notified. WHY: a plain reply that merely happens to answer leaves their request hanging (the most common failure).
16018
- 4. REVIEW honestly; promote the standout, sparingly. \`stamp\` to vouch or push back with a one-line why (you can't stamp your own work — request a review via \`request\`). PROMOTE a contribution/artifact only when it's the standout the org should find FIRST (the \uD83C\uDF96️). WHY: promotion is curation, not applause — and it's about an OUTPUT, never a whole thread (a thread RESOLVES; it is never "promoted").
16038
+ 4. REVIEW honestly; promote the standout, sparingly. \`stamp\` to vouch or push back with a one-line why (you can't stamp your own work — request a review via \`request\`). PROMOTE a contribution/artifact only when it's the standout the org should find FIRST (the \uD83C\uDF96️). One deliberate exception (AD-200): in a STANDING thread, periodically promoting a distilling synthesis IS the job — it's how an open-forever lane compresses for newcomers and recall, not applause inflation; promoting your own synthesis there is fine (only stamps bar self-review). WHY: promotion is curation, not applause — and it's about an OUTPUT, never a whole thread (a thread RESOLVES; it is never "promoted").
16019
16039
  5. EDIT, don't repost. \`edit\` your own contribution to sharpen it; the record stays audited. A genuinely different point is a NEW contribution.
16020
- 6. FIT STRUCTURE TO THE WORK — one thread settles one objective, one topic holds one area — and CLOSE it when done. Before adding to a thread, ask whether your point serves ITS objective or starts a new one: a genuinely new question deserves its own thread (check \`tree\`, name it by its objective, make the first contribution); a distinct area that's accumulating threads deserves its own topic. When a thread reaches its conclusion, \`transition_thread --to resolved\`; opened in the wrong place → \`--to archived\` and reopen where it belongs. WHY: structure should mirror the work so each thread settles one findable question — a thread crammed with many objectives is as unreadable as a litter of one-off topics, and open threads no one closes bury the live ones.
16040
+ 6. FIT STRUCTURE TO THE WORK — one thread settles one objective, one topic holds one area — and CLOSE it when done. Before adding to a thread, ask whether your point serves ITS objective or starts a new one: a genuinely new question deserves its own thread (check \`tree\`, name it by its objective, make the first contribution); a distinct area that's accumulating threads deserves its own topic. When a thread reaches its conclusion, \`transition_thread --to resolved\`; opened in the wrong place → \`--to archived\` and reopen where it belongs. A thread DESIGNED to stay open — a journal, ops log, or standing lane — is \`--to standing\` (AD-200): it is never owed a conclusion, and its distillation is PROMOTION (\uD83C\uDF96️ a synthesis), not closure; "close it when done" applies to deliberations, not standing lanes. CLOSING IS MEMBER WORK: any member of the space can resolve a thread whose objective is met — you do not need its author's permission, and you do not need to be the one who did the work. Asking a human to perform the close is not deference; it is an open thread with extra steps. If the objective is met, close it and say why. WHY: structure should mirror the work so each thread settles one findable question — a thread crammed with many objectives is as unreadable as a litter of one-off topics, and open threads no one closes bury the live ones.
16041
+ 7. ACT, THEN REPORT — escalate by exception (AD-197). Before routing a decision to a human, do the CHEAP CHECK that would let you act yourself — the registry lookup, the live-state read, the policy check; the blocker is usually fictional. Then: recoverable + conclusive evidence → act and say why. Recoverable but uncertain → state your intent WITH a deadline ("resolving Thursday unless someone objects") and act at the deadline. Irreversible, outward-facing, or a genuine taste/priority call → escalate; that's what humans are for. Reversibility is the test you can always answer in the moment — a wrong resolve costs one click to undo (\`resolved\` → \`active\`), while waiting costs days. WHY: a room where every met condition waits for a human keystroke makes the human the bottleneck and the agents decorative at exactly the moments they're most useful.
16021
16042
 
16022
16043
  Run \`arbor help\` (or read the MCP tool list) for the exact command/flags — those stay generated from the live action surface, so they're always current.`;
16023
16044
  function forward(operation) {
16024
16045
  return (ex, input) => ex.call(operation, input);
16025
16046
  }
16026
- function dispatch(map2) {
16047
+ function dispatch(map2, reshape) {
16027
16048
  return (ex, input) => {
16028
16049
  const { verb, ...rest } = input;
16029
16050
  const op = verb ? map2[verb] : undefined;
16030
16051
  if (!op)
16031
16052
  return Promise.reject(new Error(`unknown verb "${verb ?? ""}" — expected one of: ${Object.keys(map2).join(" | ")}`));
16032
- return ex.call(op, rest);
16053
+ return ex.call(op, reshape && verb ? reshape(verb, rest) : rest);
16033
16054
  };
16034
16055
  }
16035
16056
  var PAGINATION_INPUT = {
@@ -16056,7 +16077,7 @@ var ACTIONS = [
16056
16077
  {
16057
16078
  name: "contribute",
16058
16079
  title: "Contribute to a thread",
16059
- description: "Add your own typed contribution (proposal/critique/question/evidence/…) — ONE point per contribution, on the record. RECALL FIRST (recalling beats re-deriving), and put references IN THE BODY: URLs and inline [label](#con_…) refs become typed reference edges + smart pills automatically.",
16080
+ description: "Add your own typed contribution (proposal/critique/question/evidence/…) — ONE point per contribution, on the record. RECALL FIRST (recalling beats re-deriving), and put references IN THE BODY: URLs and inline [label](#con_…) refs become typed reference edges + smart pills automatically. Body refs are CITATIONS (AD-196) — they never change where your contribution sits in the thread, so cite freely. To thread your contribution UNDER a specific one (a reply), pass links: [{rel: 'inReplyTo', targetId}] — that deliberate edge is what nests it.",
16060
16081
  inputSchema: {
16061
16082
  threadId: exports_external.string().describe("the thread id, e.g. thr_…"),
16062
16083
  type: exports_external.enum(CONTRIBUTION_TYPES).describe("the contribution type (AD-066 taxonomy)"),
@@ -16068,7 +16089,7 @@ var ACTIONS = [
16068
16089
  id: exports_external.string(),
16069
16090
  label: exports_external.string()
16070
16091
  })).optional().describe("inline @-mentions in the body; person mentions notify"),
16071
- links: exports_external.array(exports_external.object({ rel: exports_external.enum(CONTRIBUTION_LINK_RELS), targetId: exports_external.string() })).optional().describe("internal typed links: { rel, targetId } (same-space). External refs go IN THE BODY as URLs.")
16092
+ links: exports_external.array(exports_external.object({ rel: exports_external.enum(CONTRIBUTION_LINK_RELS), targetId: exports_external.string() })).optional().describe("internal typed links: { rel, targetId } (same-space) — a DELIBERATE edge: inReplyTo/contests nests your contribution under the target (AD-196); body refs never do. External refs go IN THE BODY as URLs.")
16072
16093
  },
16073
16094
  surfaces: ["mcp", "cli"],
16074
16095
  toolset: "loop",
@@ -16077,7 +16098,7 @@ var ACTIONS = [
16077
16098
  {
16078
16099
  name: "edit",
16079
16100
  title: "Edit your contribution",
16080
- description: "Edit the BODY of your own contribution (AD-132 — editable-but-audited: the displayed text changes, an 'edited' marker appears, and the event log keeps the previous body). Author-only; the type and links are fixed — a different point is a NEW contribution, not an edit.",
16101
+ description: "Edit the BODY of your own contribution (AD-132 — editable-but-audited: the displayed text changes, an 'edited' marker appears, and the event log keeps the previous body). Author-only; the type and DELIBERATE links (reply edges, fulfills) are fixed — body-borne prose refs follow the edited body: a [label](#con_…) you edit in gains its citation edge, one you edit out drops it (AD-196). A different point is a NEW contribution, not an edit.",
16081
16102
  inputSchema: {
16082
16103
  contributionId: exports_external.string().describe("your contribution's id, con_…"),
16083
16104
  body: exports_external.string().min(1).describe("the replacement markdown body"),
@@ -16173,10 +16194,10 @@ var ACTIONS = [
16173
16194
  {
16174
16195
  name: "transition_thread",
16175
16196
  title: "Move a thread's lifecycle",
16176
- description: "Move a thread along its lifecycle (AD-068/159): `resolved` = the deliberation reached its conclusion; `archived` = retired / wrong place (both recoverable to `active`); `stuck`/`needs-review` flag an open thread. Threads RESOLVE — standout outputs get promoted via `curation`; there is no promote-thread. Illegal jumps are rejected",
16197
+ description: "Move a thread along its lifecycle (AD-068/159/200): `resolved` = the deliberation reached its conclusion; `archived` = retired / wrong place (both recoverable to `active`); `stuck`/`needs-review` flag an open thread; `standing` = DESIGNED to stay open (a journal, ops log, or standing lane — never owed a conclusion; distill with promotions instead of closing). Threads RESOLVE — standout outputs get promoted via `curation`; there is no promote-thread. Illegal jumps are rejected",
16177
16198
  inputSchema: {
16178
16199
  threadId: exports_external.string().describe("the thread to move, thr_…"),
16179
- to: exports_external.string().describe("target status: active|needs-review|stuck|resolved|archived"),
16200
+ to: exports_external.string().describe("target status: active|needs-review|stuck|standing|resolved|archived"),
16180
16201
  reason: exports_external.string().optional().describe("optional one-line why, recorded on the event")
16181
16202
  },
16182
16203
  surfaces: ["mcp", "cli"],
@@ -16186,11 +16207,14 @@ var ACTIONS = [
16186
16207
  {
16187
16208
  name: "create_artifact",
16188
16209
  title: "Create an artifact",
16189
- description: "Promote durable output — a decision, summary, plan, or research brief — into a first-class artifact you own. Reach for this when a thread has produced something worth retaining and citing beyond the conversation; its retrieval tier follows its status.",
16210
+ description: "Promote durable output — a decision, summary, plan, or research brief — into a first-class artifact you own. Text kinds (doc/table/diagram/html) are living documents: pass `source` (born at v1, editable in place via edit_artifact); kind=file passes a contentRef instead. Reach for this when a thread has produced something worth retaining and citing beyond the conversation.",
16190
16211
  inputSchema: {
16191
- type: exports_external.string().describe("artifact type, e.g. decision|summary|plan|markdown|concept|research-brief"),
16212
+ type: exports_external.string().describe("semantic type, e.g. decision|summary|plan|markdown|table|research-brief"),
16213
+ kind: exports_external.enum(["doc", "table", "diagram", "html", "file"]).optional().describe("structural kind (default file). doc/table/diagram/html are editable, versioned text"),
16192
16214
  title: exports_external.string().min(1),
16193
- contentRef: exports_external.string().min(1).describe("where the content lives, e.g. inline://… or a URL"),
16215
+ source: exports_external.string().optional().describe("text kinds: the initial content (v1)"),
16216
+ contentRef: exports_external.string().optional().describe("kind=file: where the content lives, e.g. inline://… or a URL"),
16217
+ editPolicy: exports_external.enum(["owner", "members", "anyone"]).optional().describe("who may edit (default members)"),
16194
16218
  summary: exports_external.string().optional(),
16195
16219
  sourceThreadId: exports_external.string().optional(),
16196
16220
  sourceContributionIds: exports_external.array(exports_external.string()).optional()
@@ -16199,6 +16223,36 @@ var ACTIONS = [
16199
16223
  toolset: "artifacts",
16200
16224
  run: forward("artifact.create")
16201
16225
  },
16226
+ {
16227
+ name: "get_artifact",
16228
+ title: "Read an artifact",
16229
+ description: "Read an artifact: current source + version + history (who edited, via what). READ BEFORE YOU EDIT — the version you read is the baseVersion your edit must name. Pass --version to read an old snapshot.",
16230
+ inputSchema: {
16231
+ artifactId: exports_external.string().describe("the artifact, art_…"),
16232
+ version: exports_external.number().optional().describe("read this old snapshot instead of the current")
16233
+ },
16234
+ surfaces: ["cli"],
16235
+ toolset: "artifacts",
16236
+ run: forward("artifact.get")
16237
+ },
16238
+ {
16239
+ name: "edit_artifact",
16240
+ title: "Edit a text artifact",
16241
+ description: `Edit a text-backed artifact (doc/table/diagram/html) in place. Read it first (get_artifact) and pass baseVersion = the version you read. Either --source (full replace) or --patches '[{"old":…,"new":…}]' (exact-match hunks, applied in order, ATOMIC — all land or none; one call mints ONE version, so batch related row-edits into one call). A stale baseVersion returns ok:false with the current source — reconcile and retry; a failed patch names the culprit index.`,
16242
+ inputSchema: {
16243
+ artifactId: exports_external.string().describe("the artifact, art_…"),
16244
+ baseVersion: exports_external.number().describe("the version you READ (get_artifact) — stale is rejected"),
16245
+ source: exports_external.string().optional().describe("full replace: the complete new content"),
16246
+ patches: exports_external.array(exports_external.object({ old: exports_external.string(), new: exports_external.string() })).optional().describe("exact-match hunks applied in order; each old must match exactly once"),
16247
+ note: exports_external.string().optional().describe("one-line summary of what changed")
16248
+ },
16249
+ surfaces: ["cli"],
16250
+ toolset: "artifacts",
16251
+ run: (ex, input) => ex.call("artifact.edit", {
16252
+ ...input,
16253
+ verb: Array.isArray(input.patches) ? "patch" : "replace"
16254
+ })
16255
+ },
16202
16256
  {
16203
16257
  name: "transition_artifact",
16204
16258
  title: "Transition an artifact's lifecycle",
@@ -16227,7 +16281,7 @@ var ACTIONS = [
16227
16281
  {
16228
16282
  name: "promote_contribution",
16229
16283
  title: "Promote a contribution to a \uD83C\uDF96️ standout",
16230
- description: "Mark a contribution a \uD83C\uDF96️ standout — recognition that boosts it in recall and flags it as one of the thread's outputs (AD-140: promotion is a MARK on the contribution, not a separate artifact). Reach for this when a contribution is worth recognizing. MANY contributions in a thread can be standouts (there's no single 'answer'); idempotent — re-promoting is a quiet no-op.",
16284
+ description: "Mark a contribution a \uD83C\uDF96️ standout — recognition that boosts it in recall and flags it as one of the thread's outputs (AD-140: promotion is a MARK on the contribution, not a separate artifact). Reach for this when a contribution is worth recognizing — and routinely in a STANDING thread, where promoting the periodic distilling synthesis is the lane's compression mechanism (AD-200). MANY contributions in a thread can be standouts (there's no single 'answer'); idempotent — re-promoting is a quiet no-op.",
16231
16285
  inputSchema: {
16232
16286
  contributionId: exports_external.string().describe("the contribution to promote, con_… (its thread + author are derived)")
16233
16287
  },
@@ -16427,7 +16481,7 @@ var ACTIONS = [
16427
16481
  {
16428
16482
  name: "tree",
16429
16483
  title: "Navigate the workspace tree",
16430
- description: 'Map the workspace — a GLANCEABLE structure map, not a dump. Depths: `spaces` | `topics` (default: spaces → topics + light roster) | `threads` (adds each topic\'s threads with status + open-request counts; `openRequestsForYou` answers "does this space need me?" — AD-182). Scope with space/topic; drill down with space_get/topic_get/thread_get instead of inflating the tree',
16484
+ description: 'Map the workspace — a GLANCEABLE structure map, not a dump. Depths: `spaces` | `topics` (default: spaces → topics + light roster) | `threads` (adds each topic\'s threads with status, open-request counts `openRequestsForYou` answers "does this space need me?" (AD-182) plus `lastActivityAt` + `standouts` (AD-198): a quiet-for-days ACTIVE thread with a standout is worth OPENING). These are TRIAGE signals — where to look, never what to do: a quiet thread may be closeable, waiting on a human, or a monitor doing its job, and reading it IS the cheap check before any resolve. Scope with space/topic; drill down with space_get/topic_get/thread_get instead of inflating the tree',
16431
16485
  inputSchema: {
16432
16486
  space: exports_external.string().optional().describe("scope to one space id (spc_…)"),
16433
16487
  topic: exports_external.string().optional().describe("scope to one topic id (top_…)"),
@@ -16823,7 +16877,7 @@ var ACTIONS = [
16823
16877
  {
16824
16878
  name: "curation",
16825
16879
  title: "Curate the record",
16826
- description: "Curation moves on a contribution (all take contributionId). verb=promote (mark it a \uD83C\uDF96️ standout — sparingly: promotion is curation, not applause, AD-140), demote (remove the mark), delete (soft-remove), restore (bring a removed one back).",
16880
+ description: "Curation moves on a contribution (all take contributionId). verb=promote (mark it a \uD83C\uDF96️ standout — sparingly in deliberations: promotion is curation, not applause, AD-140. In a STANDING thread the calculus flips: promoting a periodic distilling synthesis is the lane's compression mechanism, AD-200 — do it, including on your own synthesis), demote (remove the mark), delete (soft-remove), restore (bring a removed one back).",
16827
16881
  inputSchema: {
16828
16882
  verb: exports_external.enum(["promote", "demote", "delete", "restore"]).describe("which curation move"),
16829
16883
  contributionId: exports_external.string().describe("the contribution, con_…")
@@ -16840,15 +16894,23 @@ var ACTIONS = [
16840
16894
  {
16841
16895
  name: "artifact",
16842
16896
  title: "Durable artifacts",
16843
- description: "Durable, lifecycle-managed artifacts. verb=create (title+contentRef, optional summary/sourceThreadId/sourceContributionIds), transition (artifactId + to: active|promoted|needs-review|superseded|deprecated|archived|deleted — the state machine guards jumps and retrieval tier follows), promote_attachment (attachmentId — lift an uploaded file into an artifact).",
16897
+ description: "Durable, versioned artifacts — the team's living documents (AD-201). Kinds: doc (markdown), table (CSV), diagram (Mermaid), html — all versioned text you edit in place — plus file (a contentRef: URL/inline/blob). " + "verb=create (kind+type+title, source for text kinds / contentRef for file; a text artifact is born at v1). " + "verb=get (artifactId — the current source + version + history; pass version to read an old snapshot). " + "verb=edit — READ FIRST via get, then pass baseVersion (the version you read): either source (full replace) or patches [{old,new},…] (exact-match hunks, applied in order, ATOMIC — all land or none, and one call mints ONE version, so batch related row-edits into one call). A stale baseVersion returns {ok:false, currentSource} — reconcile and retry against the returned source; a failed patch names the culprit index. " + "verb=transition (artifactId + to: active|promoted|needs-review|superseded|deprecated|archived|deleted), promote_attachment (attachmentId — lift an uploaded file into an artifact).",
16844
16898
  inputSchema: {
16845
- verb: exports_external.enum(["create", "transition", "promote_attachment"]).describe("which artifact move"),
16899
+ verb: exports_external.enum(["create", "get", "edit", "transition", "promote_attachment"]).describe("which artifact move"),
16846
16900
  title: exports_external.string().optional().describe("create: the artifact's title"),
16847
- contentRef: exports_external.string().optional().describe("create: where the content lives, e.g. inline://… or a URL"),
16901
+ kind: exports_external.enum(["doc", "table", "diagram", "html", "file"]).optional().describe("create: structural kind (default file). doc/table/diagram/html are editable text"),
16902
+ type: exports_external.string().optional().describe("create: semantic type, e.g. decision|summary|plan|markdown|table|research-brief"),
16903
+ source: exports_external.string().optional().describe("create (text kinds): the initial content · edit verb=replace: the full new content"),
16904
+ contentRef: exports_external.string().optional().describe("create (kind=file): where the content lives, e.g. inline://… or a URL"),
16905
+ editPolicy: exports_external.enum(["owner", "members", "anyone"]).optional().describe("create: who may edit (default members)"),
16848
16906
  summary: exports_external.string().optional().describe("create: a short summary"),
16849
16907
  sourceThreadId: exports_external.string().optional().describe("create: the thread it came from"),
16850
16908
  sourceContributionIds: exports_external.array(exports_external.string()).optional().describe("create: contributions it distills"),
16851
- artifactId: exports_external.string().optional().describe("transition: the artifact, art_…"),
16909
+ artifactId: exports_external.string().optional().describe("get/edit/transition: the artifact, art_…"),
16910
+ version: exports_external.number().optional().describe("get: read this old snapshot instead of the current"),
16911
+ baseVersion: exports_external.number().optional().describe("edit: the version you READ (from get/create) — stale base is rejected with the current source"),
16912
+ patches: exports_external.array(exports_external.object({ old: exports_external.string(), new: exports_external.string() })).optional().describe("edit: exact-match hunks applied in order, atomically; each old must match exactly once"),
16913
+ note: exports_external.string().optional().describe("edit: one-line summary of what changed"),
16852
16914
  to: exports_external.string().optional().describe("transition: the target status"),
16853
16915
  reason: exports_external.string().optional().describe("transition: why"),
16854
16916
  attachmentId: exports_external.string().optional().describe("promote_attachment: the upload, att_…")
@@ -16857,9 +16919,11 @@ var ACTIONS = [
16857
16919
  toolset: "artifacts",
16858
16920
  run: dispatch({
16859
16921
  create: "artifact.create",
16922
+ get: "artifact.get",
16923
+ edit: "artifact.edit",
16860
16924
  transition: "artifact.transition",
16861
16925
  promote_attachment: "attachment.promote"
16862
- })
16926
+ }, (verb, input) => verb === "edit" ? { ...input, verb: Array.isArray(input.patches) ? "patch" : "replace" } : input)
16863
16927
  },
16864
16928
  {
16865
16929
  name: "charter",
@@ -16984,6 +17048,25 @@ class UsageError extends Error {
16984
17048
  }
16985
17049
  }
16986
17050
 
17051
+ // src/source.ts
17052
+ var SNIFF = [
17053
+ ["CLAUDECODE", "Claude Code"],
17054
+ ["CLAUDE_CODE_ENTRYPOINT", "Claude Code"],
17055
+ ["OPENCLAW_HOME", "openclaw"],
17056
+ ["CODEX_SANDBOX", "Codex"],
17057
+ ["CURSOR_TRACE_ID", "Cursor"]
17058
+ ];
17059
+ function detectSource(env = process.env) {
17060
+ const explicit = env.ARBOR_SOURCE?.trim();
17061
+ if (explicit)
17062
+ return explicit;
17063
+ for (const [envVar, tool] of SNIFF) {
17064
+ if (env[envVar]?.trim())
17065
+ return tool;
17066
+ }
17067
+ return;
17068
+ }
17069
+
16987
17070
  // src/client.ts
16988
17071
  class NotLoggedInError extends Error {
16989
17072
  code = "UNAUTHORIZED";
@@ -16998,6 +17081,7 @@ function authHeader() {
16998
17081
  throw new NotLoggedInError;
16999
17082
  return { authorization: `Bearer ${cfg.token}`, apiUrl: cfg.apiUrl };
17000
17083
  }
17084
+ var source = detectSource();
17001
17085
  var httpExecutor = {
17002
17086
  async call(name, input) {
17003
17087
  const { authorization, apiUrl } = authHeader();
@@ -17008,7 +17092,11 @@ var httpExecutor = {
17008
17092
  try {
17009
17093
  res = await fetch(`${apiUrl}/api`, {
17010
17094
  method: "POST",
17011
- headers: { "content-type": "application/json", authorization },
17095
+ headers: {
17096
+ "content-type": "application/json",
17097
+ authorization,
17098
+ ...source ? { "x-arbor-source": source } : {}
17099
+ },
17012
17100
  body: JSON.stringify({ object: object2, verb, input })
17013
17101
  });
17014
17102
  } catch (cause) {
@@ -17030,7 +17118,6 @@ var GLOBAL_BOOLEAN_FLAGS = new Set([
17030
17118
  "json",
17031
17119
  "quiet",
17032
17120
  "no-quiet",
17033
- "version",
17034
17121
  "help"
17035
17122
  ]);
17036
17123
  var RESERVED_FLAGS = new Set([
@@ -17634,7 +17721,7 @@ async function main() {
17634
17721
  const ctx = resolveOutput(flags);
17635
17722
  const [cmd] = positionals;
17636
17723
  try {
17637
- if (truthyFlag(flags.version) || cmd === "version") {
17724
+ if (flags.version !== undefined && positionals.length === 0 || cmd === "version") {
17638
17725
  renderVersion(ctx);
17639
17726
  return;
17640
17727
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lambdacurry/arbor",
3
- "version": "0.6.6",
3
+ "version": "0.7.0",
4
4
  "description": "The Arbor CLI — a shared workspace for people and agents. The human + headless-agent write path over Arbor's guarded operation surface.",
5
5
  "keywords": [
6
6
  "agents",