@mean-weasel/lineage 0.1.7 → 0.1.8

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.8
4
+
5
+ - Add durable selection packet export so agents can hand selected Lineage assets to GrowthOps without scraping UI state or copying local paths.
6
+ - Add Agent OS adoption guidance for Lineage agents and operators.
7
+ - Improve popover media previews, node actions, and image expansion controls for faster asset inspection.
8
+ - Clean up the Lineage shell navigation, toolbar, and side-panel layout for a more focused workspace.
9
+
3
10
  ## 0.1.7
4
11
 
5
12
  - Add a claim-aware lineage task queue for per-image iteration and re-roll work, including task instructions, comments, cancellation, and human override controls.
package/README.md CHANGED
@@ -60,6 +60,20 @@ Agents can inspect the current lineage graph without a claim:
60
60
  lineage agent graph --project demo-project --root <root-asset-id> --json
61
61
  ```
62
62
 
63
+ Agents can also export the current active workspace selection as a durable JSON
64
+ packet for downstream tools such as GrowthOps:
65
+
66
+ ```bash
67
+ lineage selection packet --project demo-project --channel linkedin --campaign 2026-07-launch --out ./lineage-selection-packet.json --json
68
+ ```
69
+
70
+ The packet is schema-versioned as `lineage.selection_packet.v1` and includes the
71
+ workspace/root binding, selected asset IDs, local media paths when known, S3 key
72
+ metadata when known, context labels/notes, warnings, and a stable `packet_id`.
73
+ Lineage only exports the packet. GrowthOps or another downstream tool remains
74
+ responsible for importing it, creating posts, checking media readiness, preparing
75
+ public URLs, scheduling, and recording placement receipts.
76
+
63
77
  Keep the claim fresh and pass it to mutating commands:
64
78
 
65
79
  ```bash
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/lineageCli.ts
4
- import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync3 } from "node:fs";
4
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
5
5
  import { homedir, platform } from "node:os";
6
- import { dirname as dirname3, join as join6, resolve as resolve4 } from "node:path";
6
+ import { dirname as dirname3, join as join6, resolve as resolve5 } from "node:path";
7
7
  import { spawn } from "node:child_process";
8
8
  import { fileURLToPath as fileURLToPath2 } from "node:url";
9
9
 
@@ -328,6 +328,13 @@ function catalogPath(project = defaultProject) {
328
328
  function fixtureCatalogPath(project = defaultProject) {
329
329
  return join3(repoRoot, "fixtures", cleanProject(project), "assets", "catalog.json");
330
330
  }
331
+ function resolvedCatalogPath(project = defaultProject) {
332
+ const path = catalogPath(project);
333
+ if (existsSync3(path)) return path;
334
+ const clean = cleanProject(project);
335
+ if (clean === defaultProject && existsSync3(fixtureCatalogPath(clean))) return fixtureCatalogPath(clean);
336
+ return path;
337
+ }
331
338
  function normalizeCatalog(catalog, fallbackProject = defaultProject) {
332
339
  const project = cleanProject(catalog.project || catalog.product || fallbackProject);
333
340
  const product = catalog.product || project;
@@ -570,6 +577,10 @@ function listAssets(project = defaultProject, options = {}) {
570
577
  error
571
578
  };
572
579
  }
580
+ function validateProject(project = defaultProject) {
581
+ const catalog = loadCatalog(project);
582
+ return { project: catalog.project, product: catalog.product, catalogPath: resolvedCatalogPath(project), default_bucket: catalog.default_bucket, default_region: catalog.default_region, asset_count: catalog.assets.length };
583
+ }
573
584
 
574
585
  // src/server/assetLineageDb.ts
575
586
  var require2 = createRequire(import.meta.url);
@@ -2223,6 +2234,17 @@ function seedLegacyWorkspaces(database, project) {
2223
2234
  );
2224
2235
  }
2225
2236
  }
2237
+ function listRows(database, project) {
2238
+ return database.prepare(`
2239
+ select * from lineage_workspaces
2240
+ where project_id = ?
2241
+ order by
2242
+ case status when 'active' then 0 when 'paused' then 1 else 2 end,
2243
+ active_at desc nulls last,
2244
+ updated_at desc,
2245
+ title
2246
+ `).all(project).map(rowToWorkspace);
2247
+ }
2226
2248
  function activeWorkspace(database, project) {
2227
2249
  const row = database.prepare(`
2228
2250
  select * from lineage_workspaces
@@ -2232,6 +2254,20 @@ function activeWorkspace(database, project) {
2232
2254
  `).get(project);
2233
2255
  return row ? rowToWorkspace(row) : null;
2234
2256
  }
2257
+ function listLineageWorkspaces(project) {
2258
+ const database = lineageDb();
2259
+ try {
2260
+ seedLegacyWorkspaces(database, project);
2261
+ return {
2262
+ project,
2263
+ active_workspace: activeWorkspace(database, project),
2264
+ workspaces: listRows(database, project),
2265
+ fetchedAt: nowIso()
2266
+ };
2267
+ } finally {
2268
+ database.close();
2269
+ }
2270
+ }
2235
2271
  function activeLineageWorkspaceRoot(project) {
2236
2272
  const database = lineageDb();
2237
2273
  try {
@@ -3336,13 +3372,234 @@ function importImageRerollOutput(project = defaultProject, fields) {
3336
3372
  }
3337
3373
  }
3338
3374
 
3375
+ // src/server/lineageSelectionPacket.ts
3376
+ import { createHash as createHash3 } from "node:crypto";
3377
+ import { existsSync as existsSync5, statSync as statSync4 } from "node:fs";
3378
+ import { isAbsolute, resolve as resolve4 } from "node:path";
3379
+ var LineageSelectionPacketError = class extends Error {
3380
+ constructor(message, warnings = [], errors = []) {
3381
+ super(message);
3382
+ this.warnings = warnings;
3383
+ this.errors = errors;
3384
+ }
3385
+ warnings;
3386
+ errors;
3387
+ };
3388
+ function listAllAssets(project) {
3389
+ const pageSize = 100;
3390
+ const first = listAssets(project, { page: 1, pageSize, source: "all" });
3391
+ const assets = [...first.assets];
3392
+ for (let page = 2; page <= first.pagination.totalPages; page += 1) {
3393
+ assets.push(...listAssets(project, { page, pageSize, source: "all" }).assets);
3394
+ }
3395
+ return assets;
3396
+ }
3397
+ function resolveWorkspace(project, options) {
3398
+ if (options.rootAssetId && options.workspaceId) {
3399
+ throw new LineageSelectionPacketError("Use either --root or --workspace, not both.", [], ["ambiguous_workspace"]);
3400
+ }
3401
+ const snapshot = listLineageWorkspaces(project);
3402
+ const target = options.workspaceId || options.rootAssetId;
3403
+ const workspace = target ? snapshot.workspaces.find((item) => item.id === target || item.root_asset_id === target) : snapshot.active_workspace;
3404
+ if (workspace) return workspace;
3405
+ if (options.rootAssetId) {
3406
+ return {
3407
+ id: lineageWorkspaceId(project, options.rootAssetId),
3408
+ project,
3409
+ root_asset_id: options.rootAssetId,
3410
+ title: `${options.rootAssetId} lineage`,
3411
+ status: "active",
3412
+ created_by: "system",
3413
+ created_at: "",
3414
+ updated_at: ""
3415
+ };
3416
+ }
3417
+ const message = options.workspaceId ? `Unknown lineage workspace: ${options.workspaceId}` : "No active lineage workspace. Pass --root or activate a workspace first.";
3418
+ throw new LineageSelectionPacketError(message, [], [options.workspaceId ? "unknown_workspace" : "missing_active_workspace"]);
3419
+ }
3420
+ function resolveLocalReference(reference) {
3421
+ if (!reference) return void 0;
3422
+ if (isAbsolute(reference)) return reference;
3423
+ const repoRelative = resolve4(repoRoot, reference);
3424
+ if (existsSync5(repoRelative)) return repoRelative;
3425
+ return resolve4(repoRoot, ".asset-scratch", reference);
3426
+ }
3427
+ function fileSize(path) {
3428
+ if (!path || !existsSync5(path)) return void 0;
3429
+ try {
3430
+ return statSync4(path).size;
3431
+ } catch {
3432
+ return void 0;
3433
+ }
3434
+ }
3435
+ function storageState(hasLocal, hasS3) {
3436
+ if (hasLocal && hasS3) return "local_and_s3";
3437
+ if (hasLocal) return "local_only";
3438
+ if (hasS3) return "s3_backed";
3439
+ return "unresolved";
3440
+ }
3441
+ function currentAttemptFor(node) {
3442
+ if (!node.current_attempt) return void 0;
3443
+ return {
3444
+ asset_id: node.current_attempt.asset_id,
3445
+ checksum_sha256: node.current_attempt.checksum_sha256,
3446
+ file_path: node.current_attempt.file_path,
3447
+ generation_job_id: node.current_attempt.generation_job_id,
3448
+ id: node.current_attempt.id,
3449
+ is_current: node.current_attempt.is_current,
3450
+ source: node.current_attempt.source
3451
+ };
3452
+ }
3453
+ function assetForNode(node, catalogAsset, warnings) {
3454
+ const localReference = catalogAsset?.local?.absolute_path || node.current_attempt?.file_path || node.local_path || catalogAsset?.local?.relative_path;
3455
+ const absolutePath = catalogAsset?.local?.absolute_path || resolveLocalReference(localReference);
3456
+ const localExists = absolutePath ? existsSync5(absolutePath) : false;
3457
+ const hasLocalClaim = Boolean(absolutePath || node.local_path || catalogAsset?.local?.relative_path);
3458
+ const s3Key = catalogAsset?.s3?.key || node.s3_key;
3459
+ const hasS3 = Boolean(s3Key);
3460
+ const checksum = catalogAsset?.local?.checksum_sha256 || node.current_attempt?.checksum_sha256 || node.checksum_sha256 || catalogAsset?.s3?.checksum_sha256;
3461
+ const mimeType = catalogAsset?.local?.content_type || catalogAsset?.s3?.content_type;
3462
+ const mediaType = catalogAsset?.content_type || node.media_type;
3463
+ if (hasLocalClaim && !localExists) warnings.push(`Selected asset ${node.asset_id} has a local path but the file is missing: ${absolutePath || node.local_path}`);
3464
+ if (!hasLocalClaim && !hasS3) warnings.push(`Selected asset ${node.asset_id} has neither a local file nor S3 key.`);
3465
+ if (mediaType && mediaType !== "image" && mediaType !== "gif") warnings.push(`Selected asset ${node.asset_id} is ${mediaType}, not an image/gif asset.`);
3466
+ return {
3467
+ asset_id: node.asset_id,
3468
+ campaign: catalogAsset?.campaign || node.campaign,
3469
+ channel: catalogAsset?.channel || node.channel,
3470
+ checksum_sha256: checksum,
3471
+ current_attempt: currentAttemptFor(node),
3472
+ local: {
3473
+ absolute_path: absolutePath,
3474
+ content_type: catalogAsset?.local?.content_type,
3475
+ exists: localExists,
3476
+ relative_path: catalogAsset?.local?.relative_path || node.local_path || node.current_attempt?.file_path,
3477
+ size_bytes: catalogAsset?.local?.size_bytes || fileSize(absolutePath)
3478
+ },
3479
+ media_type: mediaType,
3480
+ mime_type: mimeType,
3481
+ review_notes: node.review_notes,
3482
+ review_state: node.review_state,
3483
+ s3: {
3484
+ bucket: catalogAsset?.s3?.bucket,
3485
+ checksum_sha256: catalogAsset?.s3?.checksum_sha256,
3486
+ content_type: catalogAsset?.s3?.content_type,
3487
+ etag: catalogAsset?.s3?.etag,
3488
+ key: s3Key,
3489
+ region: catalogAsset?.s3?.region,
3490
+ size_bytes: catalogAsset?.s3?.size_bytes,
3491
+ version_id: catalogAsset?.s3?.version_id
3492
+ },
3493
+ selection_note: node.selection_note,
3494
+ source: catalogAsset?.source || node.source,
3495
+ status: catalogAsset?.status || node.status,
3496
+ storage_state: storageState(hasLocalClaim, hasS3),
3497
+ title: catalogAsset?.title || node.title || node.asset_id
3498
+ };
3499
+ }
3500
+ function packetId(packetIdentity) {
3501
+ const digest = createHash3("sha256").update(JSON.stringify(packetIdentity)).digest("hex").slice(0, 24);
3502
+ return `lineage_packet_${digest}`;
3503
+ }
3504
+ function getLineageSelectionPacket(project, options = {}) {
3505
+ const workspace = resolveWorkspace(project, options);
3506
+ const snapshot = getLineageSnapshot(project, workspace.root_asset_id);
3507
+ const catalogSummary = validateProject(project);
3508
+ const catalogAssets = listAllAssets(project);
3509
+ const catalogById = new Map(catalogAssets.map((asset) => [asset.asset_id, asset]));
3510
+ const warnings = [];
3511
+ const errors = [];
3512
+ const nodeById = new Map(snapshot.nodes.map((node) => [node.asset_id, node]));
3513
+ const selectedItems = snapshot.selections.map((selection) => ({
3514
+ asset_id: selection.asset_id,
3515
+ position: selection.position,
3516
+ selected_at: selection.selected_at,
3517
+ selection_note: selection.notes
3518
+ }));
3519
+ if (selectedItems.length === 0) warnings.push(`Lineage workspace ${workspace.id} has no selected assets.`);
3520
+ const assets = selectedItems.flatMap((item) => {
3521
+ const node = nodeById.get(item.asset_id);
3522
+ if (!node) {
3523
+ const message = `Selected asset ${item.asset_id} is not present in the resolved workspace snapshot.`;
3524
+ warnings.push(message);
3525
+ errors.push("selected_asset_outside_workspace");
3526
+ return [];
3527
+ }
3528
+ return [assetForNode(node, catalogById.get(item.asset_id), warnings)];
3529
+ });
3530
+ if (assets.some((asset) => !asset.dimensions)) warnings.push("Image dimensions are unavailable for one or more selected assets.");
3531
+ if (options.strict) {
3532
+ const strictErrors = [
3533
+ ...selectedItems.length === 0 ? ["empty_selection"] : [],
3534
+ ...assets.filter((asset) => asset.local.absolute_path && !asset.local.exists).map((asset) => `missing_local_file:${asset.asset_id}`),
3535
+ ...errors
3536
+ ];
3537
+ if (strictErrors.length > 0) {
3538
+ throw new LineageSelectionPacketError(`Lineage selection packet strict mode failed: ${strictErrors.join(", ")}`, warnings, strictErrors);
3539
+ }
3540
+ }
3541
+ const context = {
3542
+ campaign: options.campaign,
3543
+ channel: options.channel,
3544
+ labels: options.labels || [],
3545
+ notes: options.contextNotes
3546
+ };
3547
+ const identity = {
3548
+ assets: assets.map((asset) => ({
3549
+ asset_id: asset.asset_id,
3550
+ checksum_sha256: asset.checksum_sha256,
3551
+ local_path: asset.local.absolute_path,
3552
+ s3_key: asset.s3.key,
3553
+ storage_state: asset.storage_state
3554
+ })),
3555
+ context,
3556
+ project,
3557
+ root_asset_id: workspace.root_asset_id,
3558
+ schema_version: "lineage.selection_packet.v1",
3559
+ selection: selectedItems,
3560
+ workspace_id: workspace.id
3561
+ };
3562
+ return {
3563
+ assets,
3564
+ context,
3565
+ created_at: (/* @__PURE__ */ new Date()).toISOString(),
3566
+ errors,
3567
+ kind: "lineage.selection_packet",
3568
+ packet_id: packetId(identity),
3569
+ product: catalogSummary.product,
3570
+ project,
3571
+ schema_version: "lineage.selection_packet.v1",
3572
+ selection: {
3573
+ asset_ids: selectedItems.map((item) => item.asset_id),
3574
+ count: selectedItems.length,
3575
+ items: selectedItems,
3576
+ root_asset_id: workspace.root_asset_id
3577
+ },
3578
+ source: {
3579
+ app: "lineage",
3580
+ command: options.command,
3581
+ db_path: options.dbPath || process.env.LINEAGE_DB || lineageDbPath(),
3582
+ package: options.packageVersion
3583
+ },
3584
+ warnings: [...new Set(warnings)],
3585
+ workspace: {
3586
+ active_at: workspace.active_at,
3587
+ id: workspace.id,
3588
+ notes: workspace.notes,
3589
+ root_asset_id: workspace.root_asset_id,
3590
+ status: workspace.status,
3591
+ title: workspace.title
3592
+ }
3593
+ };
3594
+ }
3595
+
3339
3596
  // src/cli/lineageCli.ts
3340
3597
  var signalExitCodes = {
3341
3598
  SIGINT: 130,
3342
3599
  SIGTERM: 143
3343
3600
  };
3344
3601
  function packageRoot() {
3345
- return resolve4(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
3602
+ return resolve5(dirname3(fileURLToPath2(import.meta.url)), "..", "..");
3346
3603
  }
3347
3604
  function packageVersion() {
3348
3605
  try {
@@ -3366,6 +3623,19 @@ function readOption(args, name) {
3366
3623
  if (index >= 0) return args[index + 1];
3367
3624
  return void 0;
3368
3625
  }
3626
+ function readOptions(args, name) {
3627
+ const values = [];
3628
+ const prefix = `${name}=`;
3629
+ for (let index = 0; index < args.length; index += 1) {
3630
+ const arg = args[index];
3631
+ if (arg.startsWith(prefix)) values.push(arg.slice(prefix.length));
3632
+ else if (arg === name && args[index + 1] && !args[index + 1].startsWith("--")) {
3633
+ values.push(args[index + 1]);
3634
+ index += 1;
3635
+ }
3636
+ }
3637
+ return values;
3638
+ }
3369
3639
  function resolveStartOptions(config, args) {
3370
3640
  const runtimeDir = dataRoot(config.displayName);
3371
3641
  const rawPort = readOption(args, "--port") || process.env.PORT || String(config.defaultPort);
@@ -3389,6 +3659,7 @@ Usage:
3389
3659
  ${config.binName} next [--project <project>] [--root <asset-id>] [--db <path>] [--json]
3390
3660
  ${config.binName} brief [--project <project>] [--root <asset-id>] [--db <path>] [--json]
3391
3661
  ${config.binName} inspect --asset-id <asset-id> [--project <project>] [--db <path>] [--json]
3662
+ ${config.binName} selection packet [--project <project>] [--workspace <id-or-root>|--root <asset-id>] [--channel <channel>] [--campaign <campaign>] [--context-notes <text>] [--label <label>] [--out <path>] [--strict] [--db <path>] [--json]
3392
3663
  ${config.binName} link-child --root <asset-id> --child <asset-id> [--project <project>] [--claim-token <claim-id.secret>] [--confirm-write] [--db <path>] [--json]
3393
3664
  ${config.binName} reroll list --root <asset-id> [--project <project>] [--db <path>] [--json]
3394
3665
  ${config.binName} reroll mark --root <asset-id> --target <asset-id> [--notes <text>] [--requested-by agent|human|system] [--project <project>] [--confirm-write] [--db <path>] [--json]
@@ -3464,6 +3735,31 @@ function runLineageDataCommand(command, args) {
3464
3735
  if (!options.assetId) throw new Error("lineage inspect requires --asset-id");
3465
3736
  return getLineageSnapshot(options.project, options.assetId);
3466
3737
  }
3738
+ if (command === "selection") {
3739
+ const subcommand = positionalArgs(args)[0] || "";
3740
+ if (subcommand !== "packet") throw new Error(`Unknown selection command: ${subcommand}`);
3741
+ const labels = readOptions(args, "--label").flatMap((label) => label.split(",")).map((label) => label.trim()).filter(Boolean);
3742
+ const packet = getLineageSelectionPacket(options.project, {
3743
+ campaign: readOption(args, "--campaign"),
3744
+ channel: readOption(args, "--channel"),
3745
+ command: "lineage selection packet",
3746
+ contextNotes: readOption(args, "--context-notes") || readOption(args, "--notes"),
3747
+ dbPath: options.dbPath,
3748
+ labels,
3749
+ packageVersion: packageVersion(),
3750
+ rootAssetId: options.rootAssetId,
3751
+ strict: args.includes("--strict"),
3752
+ workspaceId: readOption(args, "--workspace") || readOption(args, "--workspace-id")
3753
+ });
3754
+ const out = readOption(args, "--out");
3755
+ if (out) {
3756
+ const outPath = resolve5(out);
3757
+ mkdirSync4(dirname3(outPath), { recursive: true });
3758
+ writeFileSync2(outPath, `${JSON.stringify(packet, null, 2)}
3759
+ `);
3760
+ }
3761
+ return packet;
3762
+ }
3467
3763
  if (command === "link-child") {
3468
3764
  if (!options.childAssetId) throw new Error("lineage link-child requires --child");
3469
3765
  return linkSelectedLineageChild(options.project, {
@@ -3613,6 +3909,13 @@ function printDataResult(command, result, json) {
3613
3909
  console.log(`Active: ${snapshot.active_asset_id}`);
3614
3910
  return;
3615
3911
  }
3912
+ if (command === "selection" && result && typeof result === "object" && "packet_id" in result) {
3913
+ const packet = result;
3914
+ console.log(`${packet.packet_id}: ${packet.selection?.count || packet.assets?.length || 0} selected asset(s)`);
3915
+ if (packet.workspace?.root_asset_id) console.log(`Root: ${packet.workspace.root_asset_id}`);
3916
+ for (const warning of packet.warnings || []) console.log(`Warning: ${warning}`);
3917
+ return;
3918
+ }
3616
3919
  if (command === "link-child" && result && typeof result === "object") {
3617
3920
  const link = result;
3618
3921
  console.log(link.message || `${link.dryRun ? "Dry run: " : ""}Link ${link.edge?.child_asset_id || "child"} from ${link.edge?.parent_asset_id || "parent"}`);
@@ -3782,7 +4085,7 @@ function start(config, args) {
3782
4085
  process.exit(1);
3783
4086
  }
3784
4087
  const serverPath = join6(packageRoot(), "dist", "server.js");
3785
- if (!existsSync5(serverPath)) {
4088
+ if (!existsSync6(serverPath)) {
3786
4089
  const message = `Missing bundled server at ${serverPath}. Run npm run build before using ${config.binName} start from a source checkout.`;
3787
4090
  if (options.json) console.error(JSON.stringify({ ok: false, error: message }, null, 2));
3788
4091
  else console.error(`${config.binName}: ${message}`);
@@ -3836,7 +4139,7 @@ function runLineageCli(config, args = process.argv.slice(2)) {
3836
4139
  start(config, normalizedArgs.slice(1));
3837
4140
  return;
3838
4141
  }
3839
- if (command === "next" || command === "brief" || command === "inspect" || command === "link-child" || command === "reroll" || command === "tasks") {
4142
+ if (command === "next" || command === "brief" || command === "inspect" || command === "selection" || command === "link-child" || command === "reroll" || command === "tasks") {
3840
4143
  const commandArgs = normalizedArgs.slice(1);
3841
4144
  const json2 = commandArgs.includes("--json");
3842
4145
  try {