@tpsdev-ai/flair 0.13.0 → 0.15.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.
- package/dist/cli-shim.cjs +66 -0
- package/dist/cli.js +293 -11
- package/dist/fabric-upgrade.js +367 -0
- package/dist/resources/A2AAdapter.js +40 -7
- package/dist/resources/OrgEvent.js +12 -2
- package/dist/resources/Presence.js +3 -9
- package/dist/resources/SemanticSearch.js +124 -2
- package/dist/resources/WorkspaceState.js +12 -2
- package/dist/resources/a2a-url.js +53 -0
- package/dist/resources/agent-auth.js +3 -10
- package/dist/resources/auth-middleware.js +3 -10
- package/dist/resources/b64.js +40 -0
- package/dist/resources/bm25-filter.js +84 -0
- package/dist/resources/bm25.js +130 -0
- package/package.json +3 -3
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* cli-shim.cts — Node-version PREFLIGHT for the Flair CLI.
|
|
5
|
+
*
|
|
6
|
+
* THIS IS THE BIN ENTRY (`package.json` "bin": { "flair": "dist/cli-shim.cjs" }).
|
|
7
|
+
*
|
|
8
|
+
* Why a separate CommonJS file instead of a guard inside cli.ts:
|
|
9
|
+
* The real CLI (dist/cli.js) is an ES module. In ESM, every top-level
|
|
10
|
+
* `import` is HOISTED and the whole module graph is LINKED + EVALUATED before
|
|
11
|
+
* the first statement in the file body runs. Flair's deps (harper-fabric-
|
|
12
|
+
* embeddings requires Node >=22, @harperfast/harper / commander require >=20)
|
|
13
|
+
* fail to load on an older engine — so a Node-version check placed even at the
|
|
14
|
+
* very top of cli.ts never executes: the import graph crashes first. That is
|
|
15
|
+
* exactly the silent-onboarding-failure bug (a Harper dev got zero output and
|
|
16
|
+
* no ~/.flair on an old Node, fixed only by upgrading).
|
|
17
|
+
*
|
|
18
|
+
* CommonJS evaluates top-to-bottom and `require()`/`import()` are evaluated
|
|
19
|
+
* lazily — so the version check below runs and prints BEFORE anything tries to
|
|
20
|
+
* load the ESM CLI or any modern dependency. Because every Node since v0.x
|
|
21
|
+
* parses and runs CommonJS, this shim is guaranteed to run and print on the
|
|
22
|
+
* oldest Node a developer could plausibly have.
|
|
23
|
+
*
|
|
24
|
+
* The check itself deliberately uses ONLY ancient-safe syntax — `var`, plain
|
|
25
|
+
* functions, string `.split`/`parseInt`, `console.error`, `process.exit`. No
|
|
26
|
+
* top-level await, no optional chaining, no template literals reaching
|
|
27
|
+
* modern-only APIs — so the guard can never become the thing that fails to
|
|
28
|
+
* parse.
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
// ── Node-version preflight (must stay parse-safe + runtime-safe on old Node) ──
|
|
32
|
+
var MIN_NODE_MAJOR = 22;
|
|
33
|
+
function flairCurrentNodeMajor() {
|
|
34
|
+
// process.versions.node is e.g. "18.20.4"; take the leading integer.
|
|
35
|
+
var raw = (process && process.versions && process.versions.node) || "0";
|
|
36
|
+
return parseInt(String(raw).split(".")[0], 10) || 0;
|
|
37
|
+
}
|
|
38
|
+
var flairNodeMajor = flairCurrentNodeMajor();
|
|
39
|
+
if (flairNodeMajor < MIN_NODE_MAJOR) {
|
|
40
|
+
// Plain string concatenation — no template literals, no chalk, nothing that
|
|
41
|
+
// could itself trip on an old engine.
|
|
42
|
+
console.error("");
|
|
43
|
+
console.error(" Flair requires Node.js >= " + MIN_NODE_MAJOR + ".");
|
|
44
|
+
console.error(" You are running Node.js " + (process.versions && process.versions.node ? process.versions.node : "(unknown)") + ".");
|
|
45
|
+
console.error("");
|
|
46
|
+
console.error(" Please upgrade Node and try again:");
|
|
47
|
+
console.error(" https://nodejs.org/ (or use nvm / fnm / volta)");
|
|
48
|
+
console.error("");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
// Node is new enough — hand off to the real ESM CLI. Dynamic import() from
|
|
52
|
+
// CommonJS is the supported ESM-from-CJS bridge. We only reach this line on a
|
|
53
|
+
// supported Node, so the import() expression is never evaluated on an engine
|
|
54
|
+
// that can't load the ESM graph.
|
|
55
|
+
import("./cli.js")
|
|
56
|
+
.then(function (mod) {
|
|
57
|
+
if (mod && typeof mod.runCli === "function") {
|
|
58
|
+
return mod.runCli();
|
|
59
|
+
}
|
|
60
|
+
// Older builds auto-ran on import via import.meta.main; nothing to call.
|
|
61
|
+
return undefined;
|
|
62
|
+
})
|
|
63
|
+
.catch(function (err) {
|
|
64
|
+
console.error(err && err.stack ? err.stack : err);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
});
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createPrivateKey, sign as nodeCryptoSign, randomUUID, randomBytes } fro
|
|
|
11
11
|
import { create as tarCreate, extract as tarExtract, list as tarList } from "tar";
|
|
12
12
|
import { keystore } from "./keystore.js";
|
|
13
13
|
import { deploy as deployToFabric, validateOptions as validateDeployOptions, buildTargetUrl as buildDeployUrl } from "./deploy.js";
|
|
14
|
+
import { fabricUpgrade } from "./fabric-upgrade.js";
|
|
14
15
|
import { detectClients, wireClaudeCode, wireCodex, wireGemini, wireCursor } from "./install/clients.js";
|
|
15
16
|
// Federation crypto helpers — inlined to avoid cross-boundary imports from
|
|
16
17
|
// src/ into resources/, which don't survive npm packaging (see also
|
|
@@ -491,11 +492,29 @@ export async function seedAgentViaOpsApi(opsPortOrUrl, agentId, pubKeyB64url, ad
|
|
|
491
492
|
// Send Basic auth whenever the caller passed an adminPass. The caller decides
|
|
492
493
|
// when to omit it (e.g., local target with authorizeLocal=true).
|
|
493
494
|
const auth = adminPass !== undefined ? Buffer.from(`${adminUser}:${adminPass}`).toString("base64") : undefined;
|
|
495
|
+
// The ops-API insert bypasses the Agent resource layer, so Agent.post()'s
|
|
496
|
+
// 1.0 Principal defaults (kind/status/displayName/admin/defaultTrustTier/type)
|
|
497
|
+
// never run. Without them a remote-seeded agent lands kind=null, status=null
|
|
498
|
+
// and is invisible to roster/presence/Office-Space queries that filter on
|
|
499
|
+
// status='active' or kind='agent' (#521). Mirror Agent.post() exactly here.
|
|
500
|
+
const now = new Date().toISOString();
|
|
494
501
|
const body = {
|
|
495
502
|
operation: "insert",
|
|
496
503
|
database: "flair",
|
|
497
504
|
table: "Agent",
|
|
498
|
-
records: [{
|
|
505
|
+
records: [{
|
|
506
|
+
id: agentId,
|
|
507
|
+
name: agentId,
|
|
508
|
+
type: "agent",
|
|
509
|
+
kind: "agent",
|
|
510
|
+
status: "active",
|
|
511
|
+
displayName: agentId,
|
|
512
|
+
admin: false,
|
|
513
|
+
defaultTrustTier: "unverified",
|
|
514
|
+
publicKey: pubKeyB64url,
|
|
515
|
+
createdAt: now,
|
|
516
|
+
updatedAt: now,
|
|
517
|
+
}],
|
|
499
518
|
};
|
|
500
519
|
const res = await fetch(url, {
|
|
501
520
|
method: "POST",
|
|
@@ -2142,6 +2161,8 @@ agent
|
|
|
2142
2161
|
.option("--admin-pass <pass>", "Admin password for registration")
|
|
2143
2162
|
.option("--keys-dir <dir>", "Directory for Ed25519 keys")
|
|
2144
2163
|
.option("--ops-port <port>", "Harper operations API port")
|
|
2164
|
+
.option("--target <url>", "Remote Flair REST URL; derives the ops API URL (port-1) to seed the Agent there (env: FLAIR_TARGET)")
|
|
2165
|
+
.option("--ops-target <url>", "Explicit ops API URL to seed the Agent on (env: FLAIR_OPS_TARGET; bypasses port derivation)")
|
|
2145
2166
|
.action(async (id, opts) => {
|
|
2146
2167
|
const httpPort = resolveHttpPort(opts);
|
|
2147
2168
|
const opsPort = resolveOpsPort(opts);
|
|
@@ -2149,6 +2170,11 @@ agent
|
|
|
2149
2170
|
const adminPass = opts.adminPass;
|
|
2150
2171
|
const adminUser = DEFAULT_ADMIN_USER;
|
|
2151
2172
|
const name = opts.name ?? id;
|
|
2173
|
+
// Where to seed the Agent record. Default is localhost (opsPort). When
|
|
2174
|
+
// --ops-target or --target is given, seed on the remote instead of localhost
|
|
2175
|
+
// (#514 — agent add could only ever hit localhost ops). Precedence matches
|
|
2176
|
+
// `flair import`: explicit --ops-target > derive from --target > localhost.
|
|
2177
|
+
const seedOpsTarget = resolveEffectiveOpsUrl({ target: opts.target, opsTarget: opts.opsTarget }) ?? opsPort;
|
|
2152
2178
|
if (!adminPass) {
|
|
2153
2179
|
console.error("Error: --admin-pass is required for agent add (needed to insert into Agent table)");
|
|
2154
2180
|
process.exit(1);
|
|
@@ -2172,8 +2198,10 @@ agent
|
|
|
2172
2198
|
pubKeyB64url = b64url(kp.publicKey);
|
|
2173
2199
|
console.log(`Keypair written: ${privPath}`);
|
|
2174
2200
|
}
|
|
2175
|
-
await seedAgentViaOpsApi(
|
|
2176
|
-
console.log(
|
|
2201
|
+
await seedAgentViaOpsApi(seedOpsTarget, id, pubKeyB64url, adminUser, adminPass);
|
|
2202
|
+
console.log(typeof seedOpsTarget === "string"
|
|
2203
|
+
? `✅ Agent '${id}' (${name}) registered (ops: ${seedOpsTarget})`
|
|
2204
|
+
: `✅ Agent '${id}' (${name}) registered`);
|
|
2177
2205
|
console.log(` Private key: ${privPath}`);
|
|
2178
2206
|
console.log(` Public key: ${pubKeyB64url}`);
|
|
2179
2207
|
});
|
|
@@ -5766,14 +5794,104 @@ statusCmd
|
|
|
5766
5794
|
console.log("\n✅ no warnings");
|
|
5767
5795
|
}
|
|
5768
5796
|
});
|
|
5797
|
+
// ─── flair upgrade --target <fabric> ────────────────────────────────────────
|
|
5798
|
+
//
|
|
5799
|
+
// One-command upgrade of a Flair instance DEPLOYED to a Harper Fabric cluster.
|
|
5800
|
+
// Mirrors `flair deploy`'s credential handling (FABRIC_USER/FABRIC_PASSWORD env
|
|
5801
|
+
// fallbacks, password-via-flag warning) and NEVER prints credentials. The
|
|
5802
|
+
// version-resolution + @harperfast/harper pin + reuse of deploy() lives in
|
|
5803
|
+
// src/fabric-upgrade.ts; this wrapper only does CLI plumbing + the confirm.
|
|
5804
|
+
async function runFabricUpgrade(opts) {
|
|
5805
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
5806
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
5807
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
5808
|
+
const fabricUser = opts.fabricUser ?? process.env.FABRIC_USER;
|
|
5809
|
+
const fabricPassword = opts.fabricPassword ?? process.env.FABRIC_PASSWORD;
|
|
5810
|
+
const check = opts.check ?? false;
|
|
5811
|
+
// Creds are not required for --check (read-only registry + best-effort GET),
|
|
5812
|
+
// but ARE required to actually deploy.
|
|
5813
|
+
if (!check && !(fabricUser && fabricPassword)) {
|
|
5814
|
+
console.error(red("flair upgrade --target: credentials required to deploy"));
|
|
5815
|
+
console.error(" pass --fabric-user + --fabric-password (or FABRIC_USER / FABRIC_PASSWORD env)");
|
|
5816
|
+
console.error(" or use --check to preview the plan without credentials");
|
|
5817
|
+
process.exit(1);
|
|
5818
|
+
}
|
|
5819
|
+
// Warn on password-via-flag (leaks to shell history). Env is preferred.
|
|
5820
|
+
if (opts.fabricPassword && !process.env.FABRIC_PASSWORD) {
|
|
5821
|
+
console.error(dim("warning: --fabric-password leaks to shell history. Prefer FABRIC_PASSWORD env."));
|
|
5822
|
+
}
|
|
5823
|
+
const upgradeOpts = {
|
|
5824
|
+
target: opts.target,
|
|
5825
|
+
project: opts.project,
|
|
5826
|
+
version: opts.version,
|
|
5827
|
+
harperVersion: opts.harperVersion,
|
|
5828
|
+
fabricUser,
|
|
5829
|
+
fabricPassword,
|
|
5830
|
+
check,
|
|
5831
|
+
restart: opts.restart !== false,
|
|
5832
|
+
replicated: opts.replicated !== false,
|
|
5833
|
+
};
|
|
5834
|
+
console.log(`${green("→")} Upgrading Fabric Flair at ${upgradeOpts.target}`);
|
|
5835
|
+
if (check)
|
|
5836
|
+
console.log(dim(" (--check: plan only, no deploy)"));
|
|
5837
|
+
try {
|
|
5838
|
+
// For a real (non-check) run, confirm first unless --yes. Building the plan
|
|
5839
|
+
// up front would double the registry round-trips; the plan prints inside
|
|
5840
|
+
// fabricUpgrade. We confirm BEFORE invoking when interactive and not --yes.
|
|
5841
|
+
if (!check && !opts.yes && process.stdin.isTTY) {
|
|
5842
|
+
const { createInterface } = await import("node:readline");
|
|
5843
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
5844
|
+
const answer = await new Promise((res) => rl.question(`Deploy a fresh ${green("@tpsdev-ai/flair")} to ${upgradeOpts.target}? [y/N] `, (a) => { rl.close(); res(a); }));
|
|
5845
|
+
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
5846
|
+
console.log("Aborted.");
|
|
5847
|
+
return;
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5850
|
+
const result = await fabricUpgrade(upgradeOpts);
|
|
5851
|
+
if (check) {
|
|
5852
|
+
console.log(`\n${green("✓")} check complete — run without --check to deploy.`);
|
|
5853
|
+
return;
|
|
5854
|
+
}
|
|
5855
|
+
if (result.plan.upToDate && !result.deployed) {
|
|
5856
|
+
console.log(`\n${green("✓")} already up to date.`);
|
|
5857
|
+
return;
|
|
5858
|
+
}
|
|
5859
|
+
console.log(`\n${green("✓")} Fabric upgrade complete.`);
|
|
5860
|
+
}
|
|
5861
|
+
catch (err) {
|
|
5862
|
+
console.error(red(`\n✗ fabric upgrade failed: ${err.message}`));
|
|
5863
|
+
const hint = err.message?.toLowerCase() ?? "";
|
|
5864
|
+
if (hint.includes("401") || hint.includes("unauthoriz")) {
|
|
5865
|
+
console.error(dim(" hint: check Fabric Studio → Cluster Settings → Admin for the admin password"));
|
|
5866
|
+
}
|
|
5867
|
+
process.exit(1);
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5769
5870
|
// ─── flair upgrade ────────────────────────────────────────────────────────────
|
|
5770
5871
|
program
|
|
5771
5872
|
.command("upgrade")
|
|
5772
|
-
.description("Upgrade Flair
|
|
5773
|
-
.option("--check", "Only check for updates, don't install")
|
|
5873
|
+
.description("Upgrade Flair — local packages by default, or a deployed Fabric with --target")
|
|
5874
|
+
.option("--check", "Only check for updates / show the plan, don't install or deploy")
|
|
5774
5875
|
.option("--restart", "Restart Flair after upgrade")
|
|
5775
5876
|
.option("--all", "Show transitive packages (e.g. flair-client) in the listing — verbose mode for debugging dep versions")
|
|
5877
|
+
// ── Fabric upgrade (--target) ────────────────────────────────────────────
|
|
5878
|
+
// When --target is passed, upgrade the Flair component DEPLOYED to that
|
|
5879
|
+
// Harper Fabric URL instead of the local npm install. Reuses `flair deploy`
|
|
5880
|
+
// under the hood with the @harperfast/harper pin baked in (flair#513).
|
|
5881
|
+
.option("--target <url>", "Upgrade the Flair deployed to this Fabric URL (not the local install)")
|
|
5882
|
+
.option("--fabric-user <user>", "Fabric admin username (env: FABRIC_USER) — for --target")
|
|
5883
|
+
.option("--fabric-password <pass>", "Fabric admin password (env: FABRIC_PASSWORD) — for --target")
|
|
5884
|
+
.option("--version <semver>", "Flair version to deploy with --target (default: latest published @tpsdev-ai/flair)")
|
|
5885
|
+
.option("--harper-version <semver>", "Pin @harperfast/harper to this version for --target (default: registry latest, floored at the flair#513 fix)")
|
|
5886
|
+
.option("--project <name>", "Fabric component name for --target", "flair")
|
|
5887
|
+
.option("--no-replicated", "Disable cluster-wide replication for --target (default: replicated=true)")
|
|
5888
|
+
.option("--yes", "Skip the confirmation prompt for --target")
|
|
5776
5889
|
.action(async (opts) => {
|
|
5890
|
+
// ── Fabric-upgrade branch ───────────────────────────────────────────────
|
|
5891
|
+
if (opts.target) {
|
|
5892
|
+
await runFabricUpgrade(opts);
|
|
5893
|
+
return;
|
|
5894
|
+
}
|
|
5777
5895
|
const { execSync, execFileSync } = await import("node:child_process");
|
|
5778
5896
|
const checkOnly = opts.check ?? false;
|
|
5779
5897
|
const showAll = opts.all ?? false;
|
|
@@ -7005,6 +7123,7 @@ memory.command("add [content]").requiredOption("--agent <id>")
|
|
|
7005
7123
|
.option("--summary <text>", "agent-set multi-sentence dense compression (3-tier chain: subject → summary → content; ops-wkoh)")
|
|
7006
7124
|
.option("--subject <text>", "one-line title / entity this memory is about")
|
|
7007
7125
|
.option("--derived-from <csv>", "Comma-separated source Memory IDs this memory was distilled/reflected from (sets Memory.derivedFrom; used by the `rem rapid` reflection loop)")
|
|
7126
|
+
.option("--visibility <value>", "Memory visibility (sets Memory.visibility). Use 'office' to share office-wide with every team agent; omit (default) to keep it private to this agent (flair#509)")
|
|
7008
7127
|
.action(async (contentArg, opts) => {
|
|
7009
7128
|
const content = contentArg ?? opts.content;
|
|
7010
7129
|
if (!content) {
|
|
@@ -7021,6 +7140,8 @@ memory.command("add [content]").requiredOption("--agent <id>")
|
|
|
7021
7140
|
body.summary = opts.summary;
|
|
7022
7141
|
if (opts.subject)
|
|
7023
7142
|
body.subject = opts.subject;
|
|
7143
|
+
if (opts.visibility)
|
|
7144
|
+
body.visibility = String(opts.visibility).trim();
|
|
7024
7145
|
if (opts.derivedFrom) {
|
|
7025
7146
|
body.derivedFrom = String(opts.derivedFrom).split(",").map((x) => x.trim()).filter(Boolean);
|
|
7026
7147
|
}
|
|
@@ -8654,11 +8775,20 @@ program
|
|
|
8654
8775
|
.option("--port <port>", "Harper HTTP port")
|
|
8655
8776
|
.option("--ops-port <port>", "Harper operations API port")
|
|
8656
8777
|
.option("--url <url>", "Flair base URL (overrides --port)")
|
|
8778
|
+
.option("--ops-target <url>", "Explicit ops API URL for the Agent seed (env: FLAIR_OPS_TARGET; bypasses port derivation). Use when --url is remote and the ops port isn't HTTP-1.")
|
|
8657
8779
|
.option("--admin-pass <pass>", "Admin password (or set FLAIR_ADMIN_PASS env)")
|
|
8658
8780
|
.option("--keys-dir <dir>", "Keys directory", defaultKeysDir())
|
|
8659
8781
|
.action(async (importPath, opts) => {
|
|
8660
8782
|
const baseUrl = opts.url ?? `http://127.0.0.1:${resolveHttpPort(opts)}`;
|
|
8661
8783
|
const opsPort = resolveOpsPort(opts);
|
|
8784
|
+
// Resolve where the Agent record is seeded. The Agent goes through the ops
|
|
8785
|
+
// API (the REST surface has no Agent POST handler), so a remote --url import
|
|
8786
|
+
// must NOT silently seed on localhost (#514 — split import). Precedence:
|
|
8787
|
+
// 1. --ops-target / FLAIR_OPS_TARGET → use directly
|
|
8788
|
+
// 2. --url given → derive ops URL from it (port-1 convention)
|
|
8789
|
+
// 3. neither → localhost opsPort (preserves local default)
|
|
8790
|
+
// The remote REST base (--url) is mapped to `target` for derivation.
|
|
8791
|
+
const seedOpsTarget = resolveEffectiveOpsUrl({ target: opts.url, opsTarget: opts.opsTarget }) ?? opsPort;
|
|
8662
8792
|
const adminPass = opts.adminPass ?? process.env.FLAIR_ADMIN_PASS ?? "";
|
|
8663
8793
|
if (!adminPass) {
|
|
8664
8794
|
console.error("Error: --admin-pass or FLAIR_ADMIN_PASS required");
|
|
@@ -8706,9 +8836,11 @@ program
|
|
|
8706
8836
|
? nacl.sign.keyPair.fromSeed(new Uint8Array(decodedSeed)).publicKey
|
|
8707
8837
|
: nacl.sign.keyPair.fromSeed(new Uint8Array(decodedSeed.subarray(0, 32))).publicKey;
|
|
8708
8838
|
const pubKeyB64url = b64url(pubKey);
|
|
8709
|
-
// Register agent via ops API
|
|
8710
|
-
await seedAgentViaOpsApi(
|
|
8711
|
-
console.log(
|
|
8839
|
+
// Register agent via ops API (remote when --url/--ops-target points off-box)
|
|
8840
|
+
await seedAgentViaOpsApi(seedOpsTarget, agentId, pubKeyB64url, DEFAULT_ADMIN_USER, adminPass);
|
|
8841
|
+
console.log(typeof seedOpsTarget === "string"
|
|
8842
|
+
? ` Agent registered (ops: ${seedOpsTarget})`
|
|
8843
|
+
: ` Agent registered`);
|
|
8712
8844
|
// Restore memories
|
|
8713
8845
|
const auth = `Basic ${Buffer.from(`${DEFAULT_ADMIN_USER}:${adminPass}`).toString("base64")}`;
|
|
8714
8846
|
let memCount = 0;
|
|
@@ -9102,9 +9234,159 @@ presence
|
|
|
9102
9234
|
console.log(` Status: ${data.presenceStatus}`);
|
|
9103
9235
|
}
|
|
9104
9236
|
});
|
|
9105
|
-
//
|
|
9106
|
-
|
|
9237
|
+
// ─── flair workspace ─────────────────────────────────────────────────────────
|
|
9238
|
+
//
|
|
9239
|
+
// Coordination write surface (ops-wmgx / Kris #510). `workspace set` writes the
|
|
9240
|
+
// agent's OWN WorkspaceState via a signed POST /WorkspaceState. Identity comes
|
|
9241
|
+
// from the Ed25519 signature — the body carries NO agentId, so an agent can only
|
|
9242
|
+
// write as itself (the WorkspaceState.post() handler overwrites agentId from the
|
|
9243
|
+
// signature regardless of body content). Mirrors `presence set`'s signed-POST shape.
|
|
9244
|
+
const MAX_WORKSPACE_FIELD_LENGTH = 2000;
|
|
9245
|
+
const workspace = program.command("workspace").description("Manage agent workspace state (The Office Space)");
|
|
9246
|
+
workspace
|
|
9247
|
+
.command("set")
|
|
9248
|
+
.description("Set your agent's current workspace state (POST /WorkspaceState)")
|
|
9249
|
+
.requiredOption("--ref <ref>", "Workspace ref (branch, worktree, or task ref)")
|
|
9250
|
+
.option("--label <text>", "Human-readable label for this workspace")
|
|
9251
|
+
.option("--provider <name>", "Provider/runtime (e.g. claude-code, openclaw)", "cli")
|
|
9252
|
+
.option("--task <id>", "Task/issue id this workspace is attached to")
|
|
9253
|
+
.option("--phase <phase>", "Current phase (e.g. design, implement, review)")
|
|
9254
|
+
.option("--summary <text>", "Short summary of current workspace state")
|
|
9255
|
+
.option("--agent <id>", "Agent ID (env: FLAIR_AGENT_ID)")
|
|
9256
|
+
.option("--port <port>", "Harper HTTP port")
|
|
9257
|
+
.option("--target <url>", "Remote Flair URL (env: FLAIR_TARGET)")
|
|
9258
|
+
.action(async (opts) => {
|
|
9259
|
+
const agentId = resolveAgentIdOrEnv(opts);
|
|
9260
|
+
if (!agentId) {
|
|
9261
|
+
console.error("Error: agent ID required. Pass --agent <id> or set FLAIR_AGENT_ID environment variable.");
|
|
9262
|
+
process.exit(1);
|
|
9263
|
+
}
|
|
9264
|
+
// Validate field lengths (free text → cap to bound the write).
|
|
9265
|
+
for (const [name, val] of [["ref", opts.ref], ["label", opts.label], ["summary", opts.summary]]) {
|
|
9266
|
+
if (val && String(val).length > MAX_WORKSPACE_FIELD_LENGTH) {
|
|
9267
|
+
console.error(`Error: --${name} exceeds ${MAX_WORKSPACE_FIELD_LENGTH} character limit (got ${String(val).length}).`);
|
|
9268
|
+
process.exit(1);
|
|
9269
|
+
}
|
|
9270
|
+
}
|
|
9271
|
+
const keyPath = resolveKeyPath(agentId);
|
|
9272
|
+
if (!keyPath) {
|
|
9273
|
+
console.error(`Error: private key not found for agent '${agentId}'. Check ~/.flair/keys/ or set FLAIR_KEY_DIR.`);
|
|
9274
|
+
process.exit(1);
|
|
9275
|
+
}
|
|
9276
|
+
const baseUrl = resolveBaseUrl(opts).replace(/\/$/, "");
|
|
9277
|
+
const auth = buildEd25519Auth(agentId, "POST", "/WorkspaceState", keyPath);
|
|
9278
|
+
// NOTE: agentId is intentionally NOT included in the body — the handler
|
|
9279
|
+
// attributes the record from the Ed25519 signature (no forging).
|
|
9280
|
+
const now = new Date().toISOString();
|
|
9281
|
+
const body = {
|
|
9282
|
+
id: `${agentId}:${opts.ref}`,
|
|
9283
|
+
ref: opts.ref,
|
|
9284
|
+
provider: opts.provider ?? "cli",
|
|
9285
|
+
timestamp: now,
|
|
9286
|
+
};
|
|
9287
|
+
if (opts.label)
|
|
9288
|
+
body.label = opts.label;
|
|
9289
|
+
if (opts.task)
|
|
9290
|
+
body.taskId = opts.task;
|
|
9291
|
+
if (opts.phase)
|
|
9292
|
+
body.phase = opts.phase;
|
|
9293
|
+
if (opts.summary)
|
|
9294
|
+
body.summary = opts.summary;
|
|
9295
|
+
const res = await fetch(`${baseUrl}/WorkspaceState`, {
|
|
9296
|
+
method: "POST",
|
|
9297
|
+
headers: { "Content-Type": "application/json", Authorization: auth },
|
|
9298
|
+
body: JSON.stringify(body),
|
|
9299
|
+
});
|
|
9300
|
+
if (!res.ok) {
|
|
9301
|
+
const text = await res.text().catch(() => "");
|
|
9302
|
+
console.error(`Error: POST /WorkspaceState failed (${res.status}): ${text}`);
|
|
9303
|
+
process.exit(1);
|
|
9304
|
+
}
|
|
9305
|
+
console.log(`✓ Workspace state updated for '${agentId}': ref=${opts.ref}${opts.phase ? `, phase=${opts.phase}` : ""}`);
|
|
9306
|
+
});
|
|
9307
|
+
// ─── flair orgevent ──────────────────────────────────────────────────────────
|
|
9308
|
+
//
|
|
9309
|
+
// Coordination write surface (ops-wmgx / Kris #510). `orgevent` publishes an
|
|
9310
|
+
// OrgEvent ATTRIBUTED to the authenticated agent via a signed POST /OrgEvent.
|
|
9311
|
+
// The event's authorId comes from the Ed25519 signature — the body carries NO
|
|
9312
|
+
// authorId, so an agent CANNOT forge another agent's events (the OrgEvent.post()
|
|
9313
|
+
// handler overwrites authorId from the signature). Mirrors `presence set`'s shape.
|
|
9314
|
+
const MAX_ORGEVENT_SUMMARY_LENGTH = 500;
|
|
9315
|
+
const MAX_ORGEVENT_DETAIL_LENGTH = 8000;
|
|
9316
|
+
program
|
|
9317
|
+
.command("orgevent")
|
|
9318
|
+
.description("Publish an org-wide coordination event attributed to your agent (POST /OrgEvent)")
|
|
9319
|
+
.requiredOption("--kind <kind>", "Event kind (e.g. coord.claim, coord.release, status)")
|
|
9320
|
+
.requiredOption("--summary <text>", "Short summary of the event")
|
|
9321
|
+
.option("--detail <text>", "Longer detail payload")
|
|
9322
|
+
.option("--scope <scope>", "Scope of the event (e.g. an agent id, repo, or 'org')")
|
|
9323
|
+
.option("--target <agentId>", "Recipient agent id (repeatable)", (val, acc) => { acc.push(val); return acc; }, [])
|
|
9324
|
+
.option("--agent <id>", "Agent ID (env: FLAIR_AGENT_ID)")
|
|
9325
|
+
.option("--port <port>", "Harper HTTP port")
|
|
9326
|
+
.option("--target-url <url>", "Remote Flair URL (env: FLAIR_TARGET)")
|
|
9327
|
+
.action(async (opts) => {
|
|
9328
|
+
const agentId = resolveAgentIdOrEnv(opts);
|
|
9329
|
+
if (!agentId) {
|
|
9330
|
+
console.error("Error: agent ID required. Pass --agent <id> or set FLAIR_AGENT_ID environment variable.");
|
|
9331
|
+
process.exit(1);
|
|
9332
|
+
}
|
|
9333
|
+
if (opts.summary && String(opts.summary).length > MAX_ORGEVENT_SUMMARY_LENGTH) {
|
|
9334
|
+
console.error(`Error: --summary exceeds ${MAX_ORGEVENT_SUMMARY_LENGTH} character limit (got ${String(opts.summary).length}).`);
|
|
9335
|
+
process.exit(1);
|
|
9336
|
+
}
|
|
9337
|
+
if (opts.detail && String(opts.detail).length > MAX_ORGEVENT_DETAIL_LENGTH) {
|
|
9338
|
+
console.error(`Error: --detail exceeds ${MAX_ORGEVENT_DETAIL_LENGTH} character limit (got ${String(opts.detail).length}).`);
|
|
9339
|
+
process.exit(1);
|
|
9340
|
+
}
|
|
9341
|
+
const keyPath = resolveKeyPath(agentId);
|
|
9342
|
+
if (!keyPath) {
|
|
9343
|
+
console.error(`Error: private key not found for agent '${agentId}'. Check ~/.flair/keys/ or set FLAIR_KEY_DIR.`);
|
|
9344
|
+
process.exit(1);
|
|
9345
|
+
}
|
|
9346
|
+
// orgevent reuses --target for recipients, so the remote-URL override is
|
|
9347
|
+
// --target-url here (env FLAIR_TARGET still honored via resolveBaseUrl).
|
|
9348
|
+
const baseUrl = resolveBaseUrl({ target: opts.targetUrl, port: opts.port }).replace(/\/$/, "");
|
|
9349
|
+
const auth = buildEd25519Auth(agentId, "POST", "/OrgEvent", keyPath);
|
|
9350
|
+
// NOTE: authorId is intentionally NOT included in the body — the handler
|
|
9351
|
+
// attributes the event from the Ed25519 signature (no forging).
|
|
9352
|
+
const body = {
|
|
9353
|
+
kind: opts.kind,
|
|
9354
|
+
summary: opts.summary,
|
|
9355
|
+
};
|
|
9356
|
+
if (opts.detail)
|
|
9357
|
+
body.detail = opts.detail;
|
|
9358
|
+
if (opts.scope)
|
|
9359
|
+
body.scope = opts.scope;
|
|
9360
|
+
if (Array.isArray(opts.target) && opts.target.length > 0)
|
|
9361
|
+
body.targetIds = opts.target;
|
|
9362
|
+
const res = await fetch(`${baseUrl}/OrgEvent`, {
|
|
9363
|
+
method: "POST",
|
|
9364
|
+
headers: { "Content-Type": "application/json", Authorization: auth },
|
|
9365
|
+
body: JSON.stringify(body),
|
|
9366
|
+
});
|
|
9367
|
+
if (!res.ok) {
|
|
9368
|
+
const text = await res.text().catch(() => "");
|
|
9369
|
+
console.error(`Error: POST /OrgEvent failed (${res.status}): ${text}`);
|
|
9370
|
+
process.exit(1);
|
|
9371
|
+
}
|
|
9372
|
+
const data = await res.json().catch(() => null);
|
|
9373
|
+
const targets = Array.isArray(opts.target) && opts.target.length > 0 ? ` → ${opts.target.join(", ")}` : "";
|
|
9374
|
+
console.log(`✓ OrgEvent published as '${agentId}': kind=${opts.kind}${targets}`);
|
|
9375
|
+
if (data?.id)
|
|
9376
|
+
console.log(` id: ${data.id}`);
|
|
9377
|
+
});
|
|
9378
|
+
// Parse argv and run the CLI. Exported so the CommonJS preflight shim
|
|
9379
|
+
// (cli-shim.cts → dist/cli-shim.cjs, the real bin entry) can invoke it after
|
|
9380
|
+
// its Node-version check passes. The shim imports this module, so import.meta.main
|
|
9381
|
+
// is false there — without this explicit entry point the CLI would load but never run.
|
|
9382
|
+
async function runCli() {
|
|
9107
9383
|
await program.parseAsync();
|
|
9108
9384
|
}
|
|
9385
|
+
// Run CLI directly when this file is the entry point — covers `node dist/cli.js`,
|
|
9386
|
+
// `bun src/cli.ts`, and the test harness (which spawns src/cli.ts under bun).
|
|
9387
|
+
// The packaged bin goes through cli-shim.cjs → runCli() instead.
|
|
9388
|
+
if (import.meta.main) {
|
|
9389
|
+
await runCli();
|
|
9390
|
+
}
|
|
9109
9391
|
// ─── Exported for testing ─────────────────────────────────────────────────────
|
|
9110
|
-
export { resolveKeyPath, buildEd25519Auth, readPortFromConfig, resolveHttpPort, resolveOpsPort, resolveTarget, resolveOpsTarget, resolveEffectiveOpsUrl, resolveOpsUrlFromTarget, signRequestBody, b64, b64url, program, api, VALID_PRESENCE_ACTIVITIES, MAX_TASK_LENGTH, isLocalBase, isLikelyRealSecret, shouldShowInlineSecretWarning, parseTokenFromFile, };
|
|
9392
|
+
export { runCli, resolveKeyPath, buildEd25519Auth, readPortFromConfig, resolveHttpPort, resolveOpsPort, resolveTarget, resolveOpsTarget, resolveEffectiveOpsUrl, resolveOpsUrlFromTarget, signRequestBody, b64, b64url, program, api, VALID_PRESENCE_ACTIVITIES, MAX_TASK_LENGTH, MAX_WORKSPACE_FIELD_LENGTH, MAX_ORGEVENT_SUMMARY_LENGTH, MAX_ORGEVENT_DETAIL_LENGTH, isLocalBase, isLikelyRealSecret, shouldShowInlineSecretWarning, parseTokenFromFile, };
|