@tpsdev-ai/flair 0.53.0 → 0.54.2

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 (97) hide show
  1. package/README.md +4 -1
  2. package/dist/build-info.json +3 -3
  3. package/dist/cli.js +1791 -15648
  4. package/dist/commands/agent.js +453 -0
  5. package/dist/commands/attention.js +121 -0
  6. package/dist/commands/backup.js +115 -0
  7. package/dist/commands/bootstrap.js +91 -0
  8. package/dist/commands/bridge.js +608 -0
  9. package/dist/commands/deploy.js +180 -0
  10. package/dist/commands/doctor.js +1665 -0
  11. package/dist/commands/export.js +110 -0
  12. package/dist/commands/federation.js +1575 -0
  13. package/dist/commands/fleet.js +73 -0
  14. package/dist/commands/grant.js +109 -0
  15. package/dist/commands/hook.js +193 -0
  16. package/dist/commands/idp.js +193 -0
  17. package/dist/commands/import.js +134 -0
  18. package/dist/commands/init.js +1203 -0
  19. package/dist/commands/inspect.js +45 -0
  20. package/dist/commands/keys.js +187 -0
  21. package/dist/commands/mcp.js +707 -0
  22. package/dist/commands/memory.js +501 -0
  23. package/dist/commands/migrate-harness-memory.js +270 -0
  24. package/dist/commands/orgevent.js +138 -0
  25. package/dist/commands/presence.js +76 -0
  26. package/dist/commands/principal.js +338 -0
  27. package/dist/commands/quality.js +1164 -0
  28. package/dist/commands/reembed.js +296 -0
  29. package/dist/commands/relationship.js +76 -0
  30. package/dist/commands/rem.js +1048 -0
  31. package/dist/commands/restore.js +130 -0
  32. package/dist/commands/search.js +244 -0
  33. package/dist/commands/service.js +315 -0
  34. package/dist/commands/session.js +184 -0
  35. package/dist/commands/soul.js +155 -0
  36. package/dist/commands/status.js +931 -0
  37. package/dist/commands/test.js +93 -0
  38. package/dist/commands/uninstall.js +143 -0
  39. package/dist/commands/upgrade.js +1628 -0
  40. package/dist/commands/workspace.js +114 -0
  41. package/dist/deploy.js +24 -0
  42. package/dist/engine-version.js +12 -4
  43. package/dist/fabric-npm-install.js +87 -0
  44. package/dist/fabric-upgrade.js +30 -15
  45. package/dist/federation-verify.js +498 -0
  46. package/dist/fleet-verify.js +144 -21
  47. package/dist/install/clients.js +167 -0
  48. package/dist/lib/auth-resolve.js +76 -1
  49. package/dist/lib/daemon-liveness.js +131 -2
  50. package/dist/lib/doctor-config-path.js +61 -0
  51. package/dist/lib/doctor-federation-driver.js +189 -0
  52. package/dist/lib/doctor-run.js +40 -0
  53. package/dist/lib/entity-vocab-cli.js +3 -3
  54. package/dist/lib/federation-pair-identity.js +47 -0
  55. package/dist/lib/launchd-repair.js +5 -4
  56. package/dist/lib/npm-registry.js +578 -0
  57. package/dist/lib/ops-api-bind.js +115 -0
  58. package/dist/lib/owned-pins.js +219 -0
  59. package/dist/lib/uninstall-purge.js +218 -0
  60. package/dist/rem/restore.js +8 -10
  61. package/dist/resources/AgentReadPosition.js +74 -0
  62. package/dist/resources/Federation.js +8 -2
  63. package/dist/resources/Memory.js +4 -3
  64. package/dist/resources/MemoryBootstrap.js +41 -25
  65. package/dist/resources/MemoryCandidate.js +5 -6
  66. package/dist/resources/OrgEventCatchup.js +126 -47
  67. package/dist/resources/agent-read-position-lib.js +83 -0
  68. package/dist/resources/agent-read-position.js +120 -0
  69. package/dist/resources/embeddings-boot.js +32 -0
  70. package/dist/resources/federation-peer-liveness.js +73 -0
  71. package/dist/resources/health.js +68 -19
  72. package/dist/resources/mcp-tools.js +48 -279
  73. package/dist/resources/memory-visibility.js +3 -3
  74. package/dist/resources/migration-boot.js +59 -18
  75. package/dist/resources/migrations/embedding-stamp.js +20 -1
  76. package/dist/resources/migrations/recheck.js +43 -0
  77. package/dist/resources/migrations/runner.js +6 -1
  78. package/dist/resources/migrations/stamp-outstanding.js +171 -0
  79. package/dist/resources/migrations/visibility-backfill.js +2 -2
  80. package/dist/resources/org-event-catchup-lib.js +47 -0
  81. package/dist/resources/record-owner-guard.js +1 -0
  82. package/dist/resources/tool-descriptors/index.js +669 -0
  83. package/dist/stamp-migration-verify.js +163 -0
  84. package/dist/stamp-outstanding.js +144 -0
  85. package/dist/version-check.js +29 -8
  86. package/docs/api-reference.md +4 -2
  87. package/docs/deploying-on-fabric.md +11 -10
  88. package/docs/deployment.md +3 -1
  89. package/docs/federation.md +19 -0
  90. package/docs/hosted-on-fabric.md +3 -3
  91. package/docs/quickstart.md +2 -1
  92. package/docs/releasing.md +20 -6
  93. package/docs/spoke-bringup.md +10 -5
  94. package/docs/standalone-local.md +3 -1
  95. package/docs/upgrade.md +25 -6
  96. package/package.json +4 -4
  97. package/schemas/agent.graphql +15 -0
@@ -0,0 +1,270 @@
1
+ import { authFetch, resolveAdminUser } from "../lib/auth-resolve.js";
2
+ import { load as parseYaml } from "js-yaml";
3
+ import { randomUUID } from "node:crypto";
4
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join, resolve, sep } from "node:path";
7
+ let cli;
8
+ /** Bind the cli-locals this module depends on. */
9
+ export function bindCli(fns) {
10
+ cli = fns;
11
+ }
12
+ function resolveHttpPort(...args) {
13
+ return cli.resolveHttpPort(...args);
14
+ }
15
+ function resolveMemoryDir(target, agentId) {
16
+ switch (target) {
17
+ case "claude-code": {
18
+ const cwd = process.cwd();
19
+ const encodedCwd = encodeURIComponent(cwd).replace(/%2F/g, "/");
20
+ const memoryDir = join(homedir(), ".claude", "projects", encodedCwd, "memory");
21
+ const resolved = resolve(memoryDir);
22
+ const expectedRoot = join(homedir(), ".claude", "projects");
23
+ if (!resolved.startsWith(expectedRoot + sep)) {
24
+ throw new Error(`Memory dir must be within ${expectedRoot}, got ${resolved}`);
25
+ }
26
+ return resolved;
27
+ }
28
+ case "openclaw": {
29
+ const cwd = process.cwd();
30
+ const encodedCwd = encodeURIComponent(cwd).replace(/%2F/g, "/");
31
+ const memoryDir = join(homedir(), ".openclaw", "projects", encodedCwd, "memory");
32
+ const resolved = resolve(memoryDir);
33
+ const expectedRoot = join(homedir(), ".openclaw", "projects");
34
+ if (!resolved.startsWith(expectedRoot + sep)) {
35
+ throw new Error(`Memory dir must be within ${expectedRoot}, got ${resolved}`);
36
+ }
37
+ return resolved;
38
+ }
39
+ case "pi": {
40
+ const cwd = process.cwd();
41
+ const encodedCwd = encodeURIComponent(cwd).replace(/%2F/g, "/");
42
+ const memoryDir = join(homedir(), ".pi", "projects", encodedCwd, "memory");
43
+ const resolved = resolve(memoryDir);
44
+ const expectedRoot = join(homedir(), ".pi", "projects");
45
+ if (!resolved.startsWith(expectedRoot + sep)) {
46
+ throw new Error(`Memory dir must be within ${expectedRoot}, got ${resolved}`);
47
+ }
48
+ return resolved;
49
+ }
50
+ default:
51
+ throw new Error(`Unknown target: ${target}. Valid: claude-code, openclaw, pi`);
52
+ }
53
+ }
54
+ /** Parse a memory file with YAML frontmatter. */
55
+ function parseMemoryFile(filePath) {
56
+ const raw = readFileSync(filePath, "utf-8");
57
+ const lines = raw.split("\n");
58
+ if (!lines[0].startsWith("---")) {
59
+ return { meta: {}, body: raw };
60
+ }
61
+ let endIdx = 1;
62
+ while (endIdx < lines.length && !lines[endIdx].startsWith("---")) {
63
+ endIdx++;
64
+ }
65
+ const frontmatterLines = lines.slice(1, endIdx);
66
+ const bodyLines = lines.slice(endIdx + 1);
67
+ const yamlContent = frontmatterLines.join("\n");
68
+ const meta = parseYaml(yamlContent) || {};
69
+ return {
70
+ meta: {
71
+ name: meta.name,
72
+ description: meta.description,
73
+ type: meta.type,
74
+ tags: Array.isArray(meta.tags) ? meta.tags : (meta.tags ? [meta.tags] : []),
75
+ },
76
+ body: bodyLines.join("\n").trim(),
77
+ };
78
+ }
79
+ /** Map memory type to durability. */
80
+ function mapDurability(type) {
81
+ switch (type) {
82
+ case "feedback":
83
+ case "reference":
84
+ return "permanent";
85
+ case "project":
86
+ case "user":
87
+ return "persistent";
88
+ default:
89
+ return "standard";
90
+ }
91
+ }
92
+ /** Extract keywords from filename for tags. */
93
+ function extractKeywordsFromFilename(filename) {
94
+ const base = filename.replace(/\.md$/, "");
95
+ const withoutType = base.replace(/^(feedback|project|reference|user)_/, "");
96
+ const parts = withoutType.split(/[_-]+/).map(p => p.toLowerCase());
97
+ const stopwords = new Set(["the", "and", "for", "with", "about", "on", "in", "to", "of", "a", "an"]);
98
+ return parts.filter(p => p.length > 2 && !stopwords.has(p));
99
+ }
100
+ export function register(program) {
101
+ // ─── flair migrate-harness-memory ───────────────────────────────────────────
102
+ /** Resolve the memory directory path for a target harness. */
103
+ program
104
+ .command("migrate-harness-memory")
105
+ .description("Migrate harness-local memories to Flair")
106
+ .requiredOption("--target <target>", "Target harness: claude-code, openclaw, pi")
107
+ .requiredOption("--agent <id>", "Agent ID to write memories under")
108
+ .option("--dry-run", "Show what would be migrated without writing")
109
+ .action(async (opts) => {
110
+ const target = opts.target;
111
+ const agentId = opts.agent;
112
+ const dryRun = !!opts.dryRun;
113
+ let memoryDir;
114
+ try {
115
+ memoryDir = resolveMemoryDir(target, agentId);
116
+ }
117
+ catch (e) {
118
+ console.error(`Error resolving memory directory: ${e.message}`);
119
+ process.exit(1);
120
+ }
121
+ if (!existsSync(memoryDir)) {
122
+ console.error(`Error: Memory directory not found: ${memoryDir}`);
123
+ process.exit(1);
124
+ }
125
+ const migratedDir = join(memoryDir, ".migrated");
126
+ if (!dryRun) {
127
+ mkdirSync(migratedDir, { recursive: true, mode: 0o700 });
128
+ }
129
+ console.log(`Migrating memories from ${memoryDir} to Flair (agentId=${agentId})`);
130
+ if (dryRun)
131
+ console.log(" (DRY RUN - no files will be modified)");
132
+ const files = readdirSync(memoryDir, { withFileTypes: true })
133
+ .filter(d => d.isFile() && d.name.endsWith(".md") && d.name !== "MEMORY.md")
134
+ .map(d => d.name)
135
+ .sort();
136
+ if (files.length === 0) {
137
+ console.log("No memory files found.");
138
+ return;
139
+ }
140
+ console.log(`Found ${files.length} memory file(s) to migrate.`);
141
+ let successCount = 0;
142
+ let skipCount = 0;
143
+ let failCount = 0;
144
+ for (const filename of files) {
145
+ const sourcePath = join(memoryDir, filename);
146
+ const migratedPath = join(migratedDir, filename);
147
+ if (existsSync(migratedPath)) {
148
+ console.log(` ${filename}: already migrated, skipping`);
149
+ skipCount++;
150
+ continue;
151
+ }
152
+ console.log(` Processing: ${filename}...`);
153
+ let parsed;
154
+ try {
155
+ parsed = parseMemoryFile(sourcePath);
156
+ }
157
+ catch (e) {
158
+ console.error(` Failed to parse: ${e.message}`);
159
+ failCount++;
160
+ continue;
161
+ }
162
+ let type = parsed.meta.type;
163
+ if (!type) {
164
+ if (filename.startsWith("feedback_"))
165
+ type = "feedback";
166
+ else if (filename.startsWith("project_"))
167
+ type = "project";
168
+ else if (filename.startsWith("reference_"))
169
+ type = "reference";
170
+ else
171
+ type = "session";
172
+ }
173
+ const durability = mapDurability(type);
174
+ const tags = [...(parsed.meta.tags || [])];
175
+ const filenameTags = extractKeywordsFromFilename(filename);
176
+ for (const t of filenameTags) {
177
+ if (!tags.includes(t))
178
+ tags.push(t);
179
+ }
180
+ tags.push(type);
181
+ const content = parsed.body;
182
+ console.log(` Type: ${type}, Durability: ${durability}`);
183
+ console.log(` Tags: ${tags.join(", ")}`);
184
+ console.log(` Content preview: ${content.slice(0, 100)}${content.length > 100 ? "..." : ""}`);
185
+ if (!dryRun) {
186
+ console.log(` Writing to Flair...`);
187
+ try {
188
+ const httpUrl = `http://127.0.0.1:${resolveHttpPort({})}`;
189
+ const agentKeyId = `${agentId}.key`;
190
+ const keysDir = join(homedir(), ".flair", "keys");
191
+ const keyPath = join(keysDir, agentKeyId);
192
+ let authSuccess = false;
193
+ if (existsSync(keyPath)) {
194
+ const memoryId = `${agentId}-${randomUUID()}`;
195
+ const body = {
196
+ id: memoryId,
197
+ agentId: agentId,
198
+ content: content,
199
+ type: type,
200
+ durability: durability,
201
+ tags: tags,
202
+ createdAt: new Date().toISOString(),
203
+ };
204
+ const memoryPath = `/Memory/${memoryId}`;
205
+ const res = await authFetch(httpUrl, agentId, keyPath, "PUT", memoryPath, body);
206
+ if (res.ok) {
207
+ authSuccess = true;
208
+ console.log(` Write to Flair successful (Ed25519 auth)`);
209
+ }
210
+ }
211
+ if (!authSuccess) {
212
+ const adminPass = process.env.FLAIR_ADMIN_PASS || process.env.HDB_ADMIN_PASSWORD || "";
213
+ if (adminPass) {
214
+ const memoryId = `${agentId}-${randomUUID()}`;
215
+ const body = {
216
+ id: memoryId,
217
+ agentId: agentId,
218
+ content: content,
219
+ type: type,
220
+ durability: durability,
221
+ tags: tags,
222
+ createdAt: new Date().toISOString(),
223
+ };
224
+ const memoryPath = `/Memory/${memoryId}`;
225
+ const auth = `Basic ${Buffer.from(`${resolveAdminUser(undefined)}:${adminPass}`).toString("base64")}`;
226
+ const res = await fetch(`${httpUrl}${memoryPath}`, {
227
+ method: "PUT",
228
+ headers: {
229
+ Authorization: auth,
230
+ "Content-Type": "application/json",
231
+ },
232
+ body: JSON.stringify(body),
233
+ signal: AbortSignal.timeout(10000),
234
+ });
235
+ if (res.ok) {
236
+ console.log(` Write to Flair successful (Basic auth)`);
237
+ }
238
+ else {
239
+ const text = await res.text();
240
+ throw new Error(`HTTP ${res.status}: ${text}`);
241
+ }
242
+ }
243
+ else {
244
+ throw new Error("No authentication method available");
245
+ }
246
+ }
247
+ renameSync(sourcePath, migratedPath);
248
+ console.log(` Moved to .migrated/${filename}`);
249
+ }
250
+ catch (e) {
251
+ console.error(` Failed: ${e.message}`);
252
+ failCount++;
253
+ continue;
254
+ }
255
+ }
256
+ else {
257
+ console.log(` [dry-run] Would write to Flair and move to .migrated/${filename}`);
258
+ }
259
+ successCount++;
260
+ }
261
+ console.log(`\nMigration complete:`);
262
+ console.log(` Processed: ${files.length}`);
263
+ console.log(` Successful: ${successCount}`);
264
+ console.log(` Skipped: ${skipCount}`);
265
+ console.log(` Failed: ${failCount}`);
266
+ if (failCount > 0) {
267
+ process.exit(1);
268
+ }
269
+ });
270
+ }
@@ -0,0 +1,138 @@
1
+ import { buildEd25519Auth, resolveKeyPath } from "../lib/auth-resolve.js";
2
+ import { randomUUID } from "node:crypto";
3
+ let cli;
4
+ /** Bind the cli-locals this module depends on. */
5
+ export function bindCli(fns) {
6
+ cli = fns;
7
+ }
8
+ function parseEntitiesOptionOrExit(...args) {
9
+ return cli.parseEntitiesOptionOrExit(...args);
10
+ }
11
+ function resolveBaseUrl(...args) {
12
+ return cli.resolveBaseUrl(...args);
13
+ }
14
+ function resolveSigningAgentId(...args) {
15
+ return cli.resolveSigningAgentId(...args);
16
+ }
17
+ export const MAX_ORGEVENT_SUMMARY_LENGTH = 500;
18
+ export const MAX_ORGEVENT_DETAIL_LENGTH = 8000;
19
+ /**
20
+ * The ONE write shape behind `flair orgevent` — signed PUT /OrgEvent/{id} —
21
+ * extracted so `flair quality --emit` (flair-quality Slice 2) reuses the
22
+ * exact same call, rather than hand-rolling a second one. Everything below
23
+ * mirrors what the `orgevent` command's action used to do inline: id
24
+ * convention (`${agentId}-${randomUUID()}`, matching flair-client's
25
+ * Memory.write()), the same Ed25519 signing (buildEd25519Auth), the same
26
+ * length guards (MAX_ORGEVENT_SUMMARY_LENGTH/MAX_ORGEVENT_DETAIL_LENGTH) so
27
+ * every caller — CLI flag or programmatic — gets them, and the same
28
+ * authorId self-declaration OrgEvent.put() verifies server-side against the
29
+ * signature (see the module doc above `orgevent`). Never throws — every
30
+ * failure mode (missing key, oversized field, non-2xx response) returns
31
+ * `{ ok: false, error }` for the caller to surface however fits its own UX.
32
+ */
33
+ export async function publishOrgEvent(params) {
34
+ if (params.summary.length > MAX_ORGEVENT_SUMMARY_LENGTH) {
35
+ return { ok: false, error: `summary exceeds ${MAX_ORGEVENT_SUMMARY_LENGTH} character limit (got ${params.summary.length})` };
36
+ }
37
+ if (params.detail && params.detail.length > MAX_ORGEVENT_DETAIL_LENGTH) {
38
+ return { ok: false, error: `detail exceeds ${MAX_ORGEVENT_DETAIL_LENGTH} character limit (got ${params.detail.length})` };
39
+ }
40
+ const keyPath = resolveKeyPath(params.agentId);
41
+ if (!keyPath) {
42
+ return { ok: false, error: `private key not found for agent '${params.agentId}'. Check ~/.flair/keys/ or set FLAIR_KEY_DIR.` };
43
+ }
44
+ const id = `${params.agentId}-${randomUUID()}`;
45
+ const auth = buildEd25519Auth(params.agentId, "PUT", `/OrgEvent/${id}`, keyPath);
46
+ const body = {
47
+ id,
48
+ authorId: params.agentId,
49
+ kind: params.kind,
50
+ summary: params.summary,
51
+ createdAt: new Date().toISOString(),
52
+ };
53
+ if (params.detail)
54
+ body.detail = params.detail;
55
+ if (params.scope)
56
+ body.scope = params.scope;
57
+ if (params.targetIds && params.targetIds.length > 0)
58
+ body.targetIds = params.targetIds;
59
+ if (params.entities && params.entities.length > 0)
60
+ body.entities = params.entities;
61
+ const res = await fetch(`${params.baseUrl}/OrgEvent/${id}`, {
62
+ method: "PUT",
63
+ headers: { "Content-Type": "application/json", Authorization: auth },
64
+ body: JSON.stringify(body),
65
+ });
66
+ if (!res.ok) {
67
+ const text = await res.text().catch(() => "");
68
+ return { ok: false, error: `PUT /OrgEvent/${id} failed (${res.status}): ${text}` };
69
+ }
70
+ const data = await res.json().catch(() => null);
71
+ return { ok: true, id: data?.id ?? id };
72
+ }
73
+ export function register(program) {
74
+ const ENTITIES_OPTION_DESCRIPTION = cli.ENTITIES_OPTION_DESCRIPTION;
75
+ // ─── flair orgevent ──────────────────────────────────────────────────────────
76
+ //
77
+ // Coordination write surface (Kris #510). `orgevent` publishes an
78
+ // OrgEvent ATTRIBUTED to the authenticated agent via a signed PUT
79
+ // /OrgEvent/{id}. authorId is asserted in the body but self-verified server
80
+ // side: OrgEvent.put() (resources/OrgEvent.ts) 403s any authorId that doesn't
81
+ // match the Ed25519 signature's agentId, so an agent still cannot forge
82
+ // another agent's events — the difference from post() is that put() checks-
83
+ // and-rejects a mismatch rather than silently overwriting it.
84
+ //
85
+ // (flair#679, measured against a real spawned Harper): table-backed resources
86
+ // only accept writes via PUT /<Table>/<id> — a bare POST /OrgEvent 405s
87
+ // ("does not have a post method implemented to handle HTTP method POST"),
88
+ // same restriction documented in resources/Memory.ts and already fixed for
89
+ // `soul set` (#498). OrgEvent.ts DOES define a post() method that
90
+ // auto-generates id/createdAt, but Harper's REST layer never routes a real
91
+ // HTTP POST to it — post() is only reachable via in-process resource
92
+ // instantiation, never the wire. put() does NOT default id/createdAt, so the
93
+ // CLI generates and supplies them itself (id convention mirrors flair-client's
94
+ // Memory.write(): `${agentId}-${randomUUID()}`).
95
+ program
96
+ .command("orgevent")
97
+ .description("Publish an org-wide coordination event attributed to your agent (PUT /OrgEvent/{id})")
98
+ .requiredOption("--kind <kind>", "Event kind (e.g. coord.claim, coord.release, status)")
99
+ .requiredOption("--summary <text>", "Short summary of the event")
100
+ .option("--detail <text>", "Longer detail payload")
101
+ .option("--scope <scope>", "Scope of the event (e.g. an agent id, repo, or 'org')")
102
+ .option("--target <agentId>", "Recipient agent id (repeatable)", (val, acc) => { acc.push(val); return acc; }, [])
103
+ .option("--entities <csv>", ENTITIES_OPTION_DESCRIPTION)
104
+ .option("--agent <id>", "Agent ID (env: FLAIR_AGENT_ID)")
105
+ .option("--port <port>", "Harper HTTP port")
106
+ .option("--target-url <url>", "Remote Flair URL (env: FLAIR_TARGET)")
107
+ .action(async (opts) => {
108
+ const { agentId } = resolveSigningAgentId(opts, "orgevent");
109
+ if (!agentId) {
110
+ console.error("Error: agent ID required. Pass --agent <id> or set FLAIR_AGENT_ID environment variable.");
111
+ process.exit(1);
112
+ }
113
+ // orgevent reuses --target for recipients, so the remote-URL override is
114
+ // --target-url here (env FLAIR_TARGET still honored via resolveBaseUrl).
115
+ const baseUrl = resolveBaseUrl({ target: opts.targetUrl, port: opts.port }).replace(/\/$/, "");
116
+ const targetIds = Array.isArray(opts.target) && opts.target.length > 0 ? opts.target : undefined;
117
+ // flair#1288: validate --entities before any key/network work; exits 1
118
+ // with the canonical format-and-type-set message on any malformed value.
119
+ const entities = opts.entities ? parseEntitiesOptionOrExit(String(opts.entities)) : undefined;
120
+ const result = await publishOrgEvent({
121
+ agentId,
122
+ baseUrl,
123
+ kind: opts.kind,
124
+ summary: opts.summary,
125
+ detail: opts.detail,
126
+ scope: opts.scope,
127
+ targetIds,
128
+ entities,
129
+ });
130
+ if (!result.ok) {
131
+ console.error(`Error: ${result.error}`);
132
+ process.exit(1);
133
+ }
134
+ const targets = targetIds ? ` → ${targetIds.join(", ")}` : "";
135
+ console.log(`✓ OrgEvent published as '${agentId}': kind=${opts.kind}${targets}`);
136
+ console.log(` id: ${result.id}`);
137
+ });
138
+ }
@@ -0,0 +1,76 @@
1
+ import { resolveKeyPath, buildEd25519Auth } from "../lib/auth-resolve.js";
2
+ let cli;
3
+ /** Bind shared CLI helpers. cli.ts calls this immediately before register(program). */
4
+ export function bindCli(fns) {
5
+ cli = fns;
6
+ }
7
+ const resolveBaseUrl = (opts) => cli.resolveBaseUrl(opts);
8
+ const resolveSigningAgentId = (opts, command) => cli.resolveSigningAgentId(opts, command);
9
+ // ─── flair presence ─────────────────────────────────────────────────────────
10
+ export const VALID_PRESENCE_ACTIVITIES = ["coding", "reviewing", "planning", "debugging", "idle"];
11
+ export const MAX_TASK_LENGTH = 120;
12
+ /** Register the `flair presence` command group (flair#1633). */
13
+ export function register(program) {
14
+ const presence = program.command("presence").description("Manage agent presence (The Office Space)");
15
+ presence
16
+ .command("set")
17
+ .description("Set your agent's current activity and task (POST /Presence)")
18
+ .option("--activity <activity>", `Activity type (${VALID_PRESENCE_ACTIVITIES.join("|")})`)
19
+ .option("--task <text>", "Short description of what you're working on")
20
+ .option("--agent <id>", "Agent ID (env: FLAIR_AGENT_ID)")
21
+ .option("--port <port>", "Harper HTTP port")
22
+ .option("--target <url>", "Remote Flair URL (env: FLAIR_TARGET)")
23
+ .action(async (opts) => {
24
+ const { agentId } = resolveSigningAgentId(opts, "presence set");
25
+ if (!agentId) {
26
+ console.error("Error: agent ID required. Pass --agent <id> or set FLAIR_AGENT_ID environment variable.");
27
+ process.exit(1);
28
+ }
29
+ // Validate activity
30
+ if (!opts.activity) {
31
+ console.error("Error: --activity is required.");
32
+ process.exit(1);
33
+ }
34
+ const activity = opts.activity;
35
+ if (!VALID_PRESENCE_ACTIVITIES.includes(activity)) {
36
+ console.error(`Error: invalid activity '${activity}'. Must be one of: ${VALID_PRESENCE_ACTIVITIES.join(", ")}`);
37
+ process.exit(1);
38
+ }
39
+ // Validate task length
40
+ const task = opts.task ?? undefined;
41
+ if (task && task.length > MAX_TASK_LENGTH) {
42
+ console.error(`Error: --task exceeds ${MAX_TASK_LENGTH} character limit (got ${task.length}).`);
43
+ process.exit(1);
44
+ }
45
+ // Resolve key path
46
+ const keyPath = resolveKeyPath(agentId);
47
+ if (!keyPath) {
48
+ console.error(`Error: private key not found for agent '${agentId}'. Check ~/.flair/keys/ or set FLAIR_KEY_DIR.`);
49
+ process.exit(1);
50
+ }
51
+ // Build auth + POST
52
+ const baseUrl = resolveBaseUrl(opts).replace(/\/$/, "");
53
+ const auth = buildEd25519Auth(agentId, "POST", "/Presence", keyPath);
54
+ const body = { activity };
55
+ if (task)
56
+ body.currentTask = task;
57
+ const res = await fetch(`${baseUrl}/Presence`, {
58
+ method: "POST",
59
+ headers: {
60
+ "Content-Type": "application/json",
61
+ Authorization: auth,
62
+ },
63
+ body: JSON.stringify(body),
64
+ });
65
+ if (!res.ok) {
66
+ const text = await res.text().catch(() => "");
67
+ console.error(`Error: POST /Presence failed (${res.status}): ${text}`);
68
+ process.exit(1);
69
+ }
70
+ const data = await res.json().catch(() => null);
71
+ console.log(`✓ Presence updated for '${agentId}': activity=${activity}${task ? `, task="${task}"` : ""}`);
72
+ if (data?.presenceStatus) {
73
+ console.log(` Status: ${data.presenceStatus}`);
74
+ }
75
+ });
76
+ }