@agent-team-foundation/first-tree-hub 0.14.9-alpha.302.1 → 0.14.9-alpha.303.1

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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as cleanWorkspaces, A as printResults, B as ClientRuntime, C as migrateLocalAgentDirs, D as checkNodeVersion, E as checkClientConfig, G as removeLocalAgent, I as restartClientService, J as ClientOrgMismatchError, K as fail, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, Q as SessionRegistry, R as stopClientService, S as createApiNameResolver, T as checkBackgroundService, U as findStaleAliases, V as handleClientOrgMismatch, W as formatStaleReason, X as FirstTreeHubSDK, Y as ClientUserMismatchError, Z as SdkError, _ as loadOnboardState, a as declineUpdate, b as saveOnboardState, c as detectInstallMode, d as reconcileLocalRuntimeProviders, et as probeCapabilities, f as uploadClientCapabilities, g as formatCheckReport, h as promptMissingFields, i as createExecuteUpdate, j as reconcileAgentConfigs, k as checkWebSocket, l as fetchLatestVersion, m as promptAddAgent, nt as configureClientLoggerForService, o as promptUpdate, p as isInteractive, q as success, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as applyClientLoggerConfig, u as installGlobalLatest, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate } from "../saas-connect-qyBF4mzh.mjs";
2
+ import { $ as cleanWorkspaces, A as printResults, B as ClientRuntime, C as migrateLocalAgentDirs, D as checkNodeVersion, E as checkClientConfig, G as removeLocalAgent, I as restartClientService, J as ClientOrgMismatchError, K as fail, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, Q as SessionRegistry, R as stopClientService, S as createApiNameResolver, T as checkBackgroundService, U as findStaleAliases, V as handleClientOrgMismatch, W as formatStaleReason, X as FirstTreeHubSDK, Y as ClientUserMismatchError, Z as SdkError, _ as loadOnboardState, a as declineUpdate, b as saveOnboardState, c as detectInstallMode, d as reconcileLocalRuntimeProviders, et as probeCapabilities, f as uploadClientCapabilities, g as formatCheckReport, h as promptMissingFields, i as createExecuteUpdate, j as reconcileAgentConfigs, k as checkWebSocket, l as fetchLatestVersion, m as promptAddAgent, nt as configureClientLoggerForService, o as promptUpdate, p as isInteractive, q as success, r as registerSaaSConnectCommand, s as PACKAGE_NAME, tt as applyClientLoggerConfig, u as installGlobalLatest, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate } from "../saas-connect-AuYDI1jx.mjs";
3
3
  import { C as resolveConfigReadonly, S as resetConfigMeta, _ as initConfig, a as loadCredentials, b as readConfigFile, c as saveAgentConfig, d as DEFAULT_DATA_DIR, f as DEFAULT_HOME_DIR, g as getConfigValue, i as ensureFreshAdminToken, m as clientConfigSchema, p as agentConfigSchema, r as ensureFreshAccessToken, s as resolveServerUrl, u as DEFAULT_CONFIG_DIR, v as loadAgents, w as setConfigValue, x as resetConfig } from "../bootstrap-DdLoOQI5.mjs";
4
4
  import { a as print, n as CLI_USER_AGENT, o as setJsonMode, r as COMMAND_VERSION, t as cliFetch } from "../cli-fetch-BGVItZxo.mjs";
5
5
  import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-CKj6zuvQ.mjs";
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as printResults, B as ClientRuntime, D as checkNodeVersion, E as checkClientConfig, F as resolveCliInvocation, H as rotateClientIdWithBackup, I as restartClientService, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, R as stopClientService, V as handleClientOrgMismatch, X as FirstTreeHubSDK, Z as SdkError, g as formatCheckReport, h as promptMissingFields, k as checkWebSocket, m as promptAddAgent, n as deriveHubUrlFromToken, p as isInteractive, t as HubUrlDerivationError, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate, z as uninstallClientService } from "./saas-connect-qyBF4mzh.mjs";
1
+ import { A as printResults, B as ClientRuntime, D as checkNodeVersion, E as checkClientConfig, F as resolveCliInvocation, H as rotateClientIdWithBackup, I as restartClientService, L as startClientService, M as getClientServiceStatus, N as installClientService, O as checkServerReachable, P as isServiceSupported, R as stopClientService, V as handleClientOrgMismatch, X as FirstTreeHubSDK, Z as SdkError, g as formatCheckReport, h as promptMissingFields, k as checkWebSocket, m as promptAddAgent, n as deriveHubUrlFromToken, p as isInteractive, t as HubUrlDerivationError, v as onboardCheck, w as checkAgentConfigs, x as runHomeMigration, y as onboardCreate, z as uninstallClientService } from "./saas-connect-AuYDI1jx.mjs";
2
2
  import { i as ensureFreshAdminToken, n as AuthRefreshRateLimitedError, o as resolveAccessToken, r as ensureFreshAccessToken, s as resolveServerUrl, t as AuthRefreshFailedError } from "./bootstrap-DdLoOQI5.mjs";
3
3
  import { i as blank, s as status } from "./cli-fetch-BGVItZxo.mjs";
4
4
  import { n as bindFeishuUser, t as bindFeishuBot } from "./feishu-CKj6zuvQ.mjs";
@@ -3880,6 +3880,34 @@ function isCanonicalDocLinkPath(path) {
3880
3880
  const normalized = normalizeDocLinkPath(path);
3881
3881
  return normalized !== null && normalized === path;
3882
3882
  }
3883
+ /**
3884
+ * Cross-agent workspace doc key.
3885
+ *
3886
+ * doc-preview snapshots from the SENDER's own workspace keep a bare,
3887
+ * base-relative key (`docs/foo.md`) — unchanged, zero-regression. A snapshot
3888
+ * of a file in ANOTHER agent's workspace needs a globally-unique key so web's
3889
+ * cache lookup is unambiguous: `<agentSlug>/<chatId>/<rel>`, i.e. the path
3890
+ * relative to the shared `workspaces/` common root (minus that root). Runtime
3891
+ * builds it when it snapshots a sibling-workspace file; web reconstructs the
3892
+ * same key from a scanned token to match the snapshot; server parses it to
3893
+ * re-check the owner is a chat participant. All three must agree, so the
3894
+ * build/parse logic lives here next to `normalizeDocLinkPath`.
3895
+ *
3896
+ * `chatId` scopes the key to a single chat: a `workspaces/<X>/<chatId>` dir
3897
+ * only exists when X has run a session in that chat, so the chatId segment is
3898
+ * the structural fence that keeps one chat from previewing another chat's
3899
+ * private workspace docs.
3900
+ */
3901
+ function buildWorkspaceDocKey(agentSlug, chatId, rel) {
3902
+ const slug = agentSlug.trim();
3903
+ const chat = chatId.trim();
3904
+ if (!slug || !chat || slug.includes("/") || chat.includes("/")) return null;
3905
+ if (slug.startsWith(".") || chat.startsWith(".")) return null;
3906
+ const relNorm = normalizeDocLinkPath(rel);
3907
+ if (!relNorm) return null;
3908
+ const key = `${slug}/${chat}/${relNorm}`;
3909
+ return isCanonicalDocLinkPath(key) ? key : null;
3910
+ }
3883
3911
  const adapterPlatformSchema = z.enum([
3884
3912
  "feishu",
3885
3913
  "slack",
@@ -10192,38 +10220,7 @@ var Deduplicator = class {
10192
10220
  return this.seen.size;
10193
10221
  }
10194
10222
  };
10195
- /**
10196
- * Scan an outbound agent message for `.md` path mentions (inline markdown
10197
- * links `[text](path.md)` AND bare `path.md` tokens) and turn each
10198
- * safely-resolvable target into a snapshot of the file's contents at send
10199
- * time.
10200
- *
10201
- * Resolution is constrained to `root`. A path resolves when its real target
10202
- * is a regular `.md` file physically inside the worktree with no hidden
10203
- * segment. Two written forms resolve:
10204
- * - workspace-relative (`docs/foo.md`, `./docs/foo.md`); and
10205
- * - **absolute paths that land inside `root`** (`<root>/docs/foo.md`).
10206
- * Web cannot map an absolute path back to a snapshot key (it does not
10207
- * know `root`), so for every resolved token whose written form is not
10208
- * already the canonical relative path, we **rewrite that span in the
10209
- * outbound text** to the canonical relative path (preserving any
10210
- * `:line[:col]` suffix). The caller sends `rewrittenText`, and web's
10211
- * unchanged re-scan then sees a relative token, matches the snapshot,
10212
- * and renders the preview link. This keeps web/server/schema untouched
10213
- * and is immune to server-side body rewrites (e.g. `@mention` prepend)
10214
- * because web re-scans rather than trusting byte offsets.
10215
- *
10216
- * Anything that escapes the worktree (absolute path resolving OUTSIDE root,
10217
- * `..` traversal, symlink pointing to a hidden segment inside or outside
10218
- * root), hides (`.dotfile` / `.dotdir`, `.agent/`), or is missing is dropped
10219
- * — the caller's message still goes through, the offending mention simply
10220
- * stays plain text in the UI (and is left untouched in `rewrittenText`).
10221
- *
10222
- * The shared schema enforces canonical path form and the server re-validates
10223
- * byte budgets + sha256 so a misbehaving runtime cannot lodge mismatched
10224
- * data.
10225
- */
10226
- async function buildMessageDocumentSnapshots(text, root) {
10223
+ async function buildMessageDocumentSnapshots(text, root, fence) {
10227
10224
  const occurrences = collectDocPathOccurrences(text);
10228
10225
  if (occurrences.length === 0) return {
10229
10226
  docs: [],
@@ -10236,28 +10233,52 @@ async function buildMessageDocumentSnapshots(text, root) {
10236
10233
  skipped: occurrences.length,
10237
10234
  rewrittenText: text
10238
10235
  };
10239
- const resolved = await Promise.all(occurrences.map(async (occ) => ({
10240
- ...occ,
10241
- canonical: await canonicalizeWorkspacePath(rootReal, occ.writtenPath)
10242
- })));
10236
+ const workspacesRootReal = fence ? await safeRealpath(fence.workspacesRoot) : null;
10237
+ const resolved = await Promise.all(occurrences.map(async (occ) => {
10238
+ const selfKey = await canonicalizeWorkspacePath(rootReal, occ.writtenPath);
10239
+ if (selfKey) return {
10240
+ ...occ,
10241
+ kind: "self",
10242
+ key: selfKey,
10243
+ file: null,
10244
+ shortForm: ""
10245
+ };
10246
+ if (workspacesRootReal && fence && isAbsolute(occ.writtenPath)) {
10247
+ const cross = await resolveCrossWorkspaceDoc(workspacesRootReal, fence, occ.writtenPath);
10248
+ if (cross) return {
10249
+ ...occ,
10250
+ kind: "cross",
10251
+ key: cross.key,
10252
+ file: cross.file,
10253
+ shortForm: cross.shortForm
10254
+ };
10255
+ }
10256
+ return {
10257
+ ...occ,
10258
+ kind: null,
10259
+ key: null,
10260
+ file: null,
10261
+ shortForm: ""
10262
+ };
10263
+ }));
10243
10264
  const docs = [];
10244
10265
  let totalBytes = 0;
10245
10266
  let skipped = 0;
10246
10267
  const seen = /* @__PURE__ */ new Set();
10247
10268
  const snapshotted = /* @__PURE__ */ new Set();
10248
10269
  for (const occ of resolved) {
10249
- const canonical = occ.canonical;
10250
- if (!canonical || !canonical.toLowerCase().endsWith(".md")) {
10270
+ const key = occ.key;
10271
+ if (!key || !key.toLowerCase().endsWith(".md")) {
10251
10272
  skipped += 1;
10252
10273
  continue;
10253
10274
  }
10254
- if (seen.has(canonical)) continue;
10255
- seen.add(canonical);
10275
+ if (seen.has(key)) continue;
10276
+ seen.add(key);
10256
10277
  if (docs.length >= 5) {
10257
10278
  skipped += 1;
10258
10279
  continue;
10259
10280
  }
10260
- const file = await resolveWorkspaceFile(rootReal, canonical);
10281
+ const file = occ.kind === "cross" ? occ.file : await resolveWorkspaceFile(rootReal, key);
10261
10282
  if (!file) {
10262
10283
  skipped += 1;
10263
10284
  continue;
@@ -10280,23 +10301,36 @@ async function buildMessageDocumentSnapshots(text, root) {
10280
10301
  }
10281
10302
  const sha256 = createHash("sha256").update(content, "utf8").digest("hex");
10282
10303
  docs.push({
10283
- path: canonical,
10304
+ path: key,
10284
10305
  sha256,
10285
10306
  size,
10286
10307
  content
10287
10308
  });
10288
10309
  totalBytes += size;
10289
- snapshotted.add(canonical);
10310
+ snapshotted.add(key);
10290
10311
  } catch {
10291
10312
  skipped += 1;
10292
10313
  }
10293
10314
  }
10315
+ const selfKeys = /* @__PURE__ */ new Set();
10316
+ for (const occ of resolved) if (occ.kind === "self" && occ.key && snapshotted.has(occ.key)) selfKeys.add(occ.key);
10294
10317
  const rewrites = [];
10295
- for (const occ of resolved) if (occ.canonical && isAbsolute(occ.writtenPath) && snapshotted.has(occ.canonical)) rewrites.push({
10296
- start: occ.start,
10297
- end: occ.end,
10298
- replacement: `${occ.canonical}${occ.lineSuffix}`
10299
- });
10318
+ for (const occ of resolved) {
10319
+ if (!occ.key || !snapshotted.has(occ.key)) continue;
10320
+ if (occ.kind === "self" && isAbsolute(occ.writtenPath)) rewrites.push({
10321
+ start: occ.start,
10322
+ end: occ.end,
10323
+ replacement: `${occ.key}${occ.lineSuffix}`
10324
+ });
10325
+ else if (occ.kind === "cross") {
10326
+ const replacement = selfKeys.has(occ.shortForm) ? occ.key : occ.shortForm;
10327
+ rewrites.push({
10328
+ start: occ.start,
10329
+ end: occ.end,
10330
+ replacement: `${replacement}${occ.lineSuffix}`
10331
+ });
10332
+ }
10333
+ }
10300
10334
  return {
10301
10335
  docs,
10302
10336
  skipped,
@@ -10395,6 +10429,44 @@ async function canonicalizeWorkspacePath(rootReal, writtenPath) {
10395
10429
  return normalizeDocLinkPath(relative(rootReal, real));
10396
10430
  }
10397
10431
  /**
10432
+ * Resolve an ABSOLUTE `.md` path that points into a DIFFERENT agent's
10433
+ * workspace under the shared common root. Returns the global snapshot key
10434
+ * `<ownerSlug>/<chatId>/<rel>`, the realpath'd file to read, and the short
10435
+ * `<ownerSlug>/<rel>` form to rewrite into the outbound text — or null when
10436
+ * the path is outside the common root, belongs to another chat, hides, is the
10437
+ * sender's own workspace (handled as a self path), is not a regular `.md`
10438
+ * file, or cannot be realpath'd.
10439
+ *
10440
+ * realpath runs BEFORE the containment check so an ancestor symlink cannot
10441
+ * fake "inside the common root" — same discipline as the self-path resolver.
10442
+ */
10443
+ async function resolveCrossWorkspaceDoc(workspacesRootReal, fence, absPath) {
10444
+ const real = await safeRealpath(absPath);
10445
+ if (!real) return null;
10446
+ const prefix = workspacesRootReal.endsWith(sep) ? workspacesRootReal : workspacesRootReal + sep;
10447
+ if (!real.startsWith(prefix)) return null;
10448
+ const segments = relative(workspacesRootReal, real).split(sep).filter((s) => s.length > 0 && s !== ".");
10449
+ if (segments.length < 3) return null;
10450
+ if (segments.some((s) => s.startsWith("."))) return null;
10451
+ const [ownerSlug, segChatId, ...rest] = segments;
10452
+ if (!ownerSlug || !segChatId) return null;
10453
+ if (segChatId !== fence.chatId) return null;
10454
+ if (ownerSlug === fence.selfSlug) return null;
10455
+ const rel = rest.join("/");
10456
+ const key = buildWorkspaceDocKey(ownerSlug, segChatId, rel);
10457
+ if (!key || !key.toLowerCase().endsWith(".md")) return null;
10458
+ try {
10459
+ if (!(await stat(real)).isFile()) return null;
10460
+ } catch {
10461
+ return null;
10462
+ }
10463
+ return {
10464
+ key,
10465
+ file: real,
10466
+ shortForm: `${ownerSlug}/${rel}`
10467
+ };
10468
+ }
10469
+ /**
10398
10470
  * Resolve a canonical workspace-relative link target against `rootReal`.
10399
10471
  * Returns the real path of a regular `.md` file that is **physically** inside
10400
10472
  * the worktree AND whose real path contains no hidden segments. The second
@@ -10434,7 +10506,11 @@ function createResultSink(deps) {
10434
10506
  let content = text;
10435
10507
  const documentBasePath = await deps.getDocumentBasePath?.();
10436
10508
  if (documentBasePath) try {
10437
- const { docs, skipped, rewrittenText } = await buildMessageDocumentSnapshots(text, documentBasePath);
10509
+ const { docs, skipped, rewrittenText } = await buildMessageDocumentSnapshots(text, documentBasePath, deps.workspacesRoot && deps.selfSlug ? {
10510
+ workspacesRoot: deps.workspacesRoot,
10511
+ chatId: deps.chatId,
10512
+ selfSlug: deps.selfSlug
10513
+ } : void 0);
10438
10514
  content = rewrittenText;
10439
10515
  if (docs.length > 0) metadata.documentContext = documentContextSchema.parse({
10440
10516
  kind: "snapshot",
@@ -11071,7 +11147,9 @@ var SessionManager = class {
11071
11147
  this.currentTrigger.delete(chatId);
11072
11148
  },
11073
11149
  log,
11074
- getDocumentBasePath: () => this.resolveDocumentBasePath(log, chatId)
11150
+ getDocumentBasePath: () => this.resolveDocumentBasePath(log, chatId),
11151
+ workspacesRoot: dirname(this.config.handlerConfig.workspaceRoot),
11152
+ selfSlug: basename(this.config.handlerConfig.workspaceRoot)
11075
11153
  });
11076
11154
  const envCtx = {
11077
11155
  sdk: this.config.sdk,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-team-foundation/first-tree-hub",
3
- "version": "0.14.9-alpha.302.1",
3
+ "version": "0.14.9-alpha.303.1",
4
4
  "type": "module",
5
5
  "description": "First Tree Hub — unified CLI for client and agent management",
6
6
  "exports": {