@hasna/instructions 0.4.19 → 0.4.21

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 (36) hide show
  1. package/README.md +12 -4
  2. package/dist/cli/add-reference-update.test.d.ts +2 -0
  3. package/dist/cli/add-reference-update.test.d.ts.map +1 -0
  4. package/dist/cli/doctor-reference-duplicates.test.d.ts +2 -0
  5. package/dist/cli/doctor-reference-duplicates.test.d.ts.map +1 -0
  6. package/dist/cli/index.js +366 -103
  7. package/dist/cli/profile-reads.test.d.ts +2 -0
  8. package/dist/cli/profile-reads.test.d.ts.map +1 -0
  9. package/dist/cli/profile-update.test.d.ts +2 -0
  10. package/dist/cli/profile-update.test.d.ts.map +1 -0
  11. package/dist/data/config-store.d.ts +10 -1
  12. package/dist/data/config-store.d.ts.map +1 -1
  13. package/dist/db/profiles.d.ts +4 -1
  14. package/dist/db/profiles.d.ts.map +1 -1
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +226 -30
  18. package/dist/lib/bounded-read.d.ts +7 -0
  19. package/dist/lib/bounded-read.d.ts.map +1 -0
  20. package/dist/lib/config-target-identity.d.ts +76 -0
  21. package/dist/lib/config-target-identity.d.ts.map +1 -1
  22. package/dist/lib/project-dashboard-standard.d.ts +1 -1
  23. package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
  24. package/dist/mcp/index.js +314 -127
  25. package/dist/mcp/server.d.ts.map +1 -1
  26. package/dist/server/index.js +246 -36
  27. package/dist/server/openapi.d.ts +246 -20
  28. package/dist/server/openapi.d.ts.map +1 -1
  29. package/dist/server/profile-contract.test.d.ts +2 -0
  30. package/dist/server/profile-contract.test.d.ts.map +1 -0
  31. package/dist/server/v1.d.ts.map +1 -1
  32. package/dist/storage/cloud-store.d.ts +8 -1
  33. package/dist/storage/cloud-store.d.ts.map +1 -1
  34. package/dist/types/index.d.ts +24 -0
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -2649,6 +2649,84 @@ var init_machine = __esm(() => {
2649
2649
  init_template();
2650
2650
  });
2651
2651
 
2652
+ // src/lib/compact-output.ts
2653
+ function parseLimit(value, fallback = DEFAULT_LIST_LIMIT, max = MAX_LIST_LIMIT) {
2654
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
2655
+ if (!Number.isFinite(parsed) || parsed <= 0)
2656
+ return fallback;
2657
+ return Math.min(Math.floor(parsed), max);
2658
+ }
2659
+ function parseCursor(value) {
2660
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
2661
+ if (!Number.isFinite(parsed) || parsed < 0)
2662
+ return 0;
2663
+ return Math.floor(parsed);
2664
+ }
2665
+ function paginate(items, opts = {}) {
2666
+ const limit = parseLimit(opts.limit, opts.defaultLimit ?? DEFAULT_LIST_LIMIT, opts.maxLimit ?? MAX_LIST_LIMIT);
2667
+ const cursor = parseCursor(opts.cursor);
2668
+ const pageItems = items.slice(cursor, cursor + limit);
2669
+ const nextCursor = cursor + pageItems.length < items.length ? cursor + pageItems.length : null;
2670
+ return {
2671
+ items: pageItems,
2672
+ total: items.length,
2673
+ limit,
2674
+ cursor,
2675
+ next_cursor: nextCursor,
2676
+ has_more: nextCursor !== null
2677
+ };
2678
+ }
2679
+ function truncateText(value, max = 80) {
2680
+ const text = (value ?? "").replace(/\s+/g, " ").trim();
2681
+ if (text.length <= max)
2682
+ return text;
2683
+ if (max <= 3)
2684
+ return text.slice(0, max);
2685
+ return `${text.slice(0, max - 3)}...`;
2686
+ }
2687
+ function truncateMiddle(value, max = 80) {
2688
+ const text = (value ?? "").replace(/\s+/g, " ").trim();
2689
+ if (text.length <= max)
2690
+ return text;
2691
+ if (max <= 3)
2692
+ return text.slice(0, max);
2693
+ const head = Math.ceil((max - 3) * 0.55);
2694
+ const tail = Math.floor((max - 3) * 0.45);
2695
+ return `${text.slice(0, head)}...${text.slice(text.length - tail)}`;
2696
+ }
2697
+ var DEFAULT_LIST_LIMIT = 20, MAX_LIST_LIMIT = 100;
2698
+
2699
+ // src/lib/bounded-read.ts
2700
+ function normalizeBoundedReadOptions(options = {}) {
2701
+ return {
2702
+ limit: parseLimit(options.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT),
2703
+ cursor: parseCursor(options.cursor)
2704
+ };
2705
+ }
2706
+ function boundedReadPage(items, total, options = {}) {
2707
+ const { limit, cursor } = normalizeBoundedReadOptions(options);
2708
+ if (items.length > limit) {
2709
+ throw new Error(`bounded read returned ${items.length} rows for limit ${limit}`);
2710
+ }
2711
+ const consumed = cursor + items.length;
2712
+ const complete = consumed >= total;
2713
+ if (!complete && items.length === 0) {
2714
+ throw new Error(`bounded read did not advance at cursor ${cursor} of ${total}`);
2715
+ }
2716
+ return {
2717
+ items,
2718
+ total,
2719
+ limit,
2720
+ cursor,
2721
+ next_cursor: complete ? null : consumed,
2722
+ has_more: !complete,
2723
+ complete,
2724
+ truncated: false,
2725
+ source_bounded: true
2726
+ };
2727
+ }
2728
+ var init_bounded_read = () => {};
2729
+
2652
2730
  // src/db/profiles.ts
2653
2731
  function rowToProfile(row) {
2654
2732
  return {
@@ -2692,9 +2770,12 @@ function getProfile(idOrSlug, db) {
2692
2770
  throw new ProfileNotFoundError(idOrSlug);
2693
2771
  return rowToProfile(row);
2694
2772
  }
2695
- function listProfiles(db) {
2773
+ function listProfilesPage(options = {}, db) {
2696
2774
  const d = db || getDatabase();
2697
- return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
2775
+ const normalized = normalizeBoundedReadOptions(options);
2776
+ const total = d.query("SELECT COUNT(*) AS total FROM profiles").get()?.total ?? 0;
2777
+ const rows = d.query("SELECT * FROM profiles ORDER BY name LIMIT ? OFFSET ?").all(normalized.limit, normalized.cursor).map(rowToProfile);
2778
+ return boundedReadPage(rows, total, normalized);
2698
2779
  }
2699
2780
  function updateProfile(idOrSlug, input, db) {
2700
2781
  const d = db || getDatabase();
@@ -2739,14 +2820,13 @@ function removeConfigFromProfile(profileIdOrSlug, configId, db) {
2739
2820
  const profile = getProfile(profileIdOrSlug, d);
2740
2821
  d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
2741
2822
  }
2742
- function getProfileConfigs(profileIdOrSlug, db) {
2823
+ function getProfileConfigsPage(profileIdOrSlug, options = {}, db) {
2743
2824
  const d = db || getDatabase();
2744
2825
  const profile = getProfile(profileIdOrSlug, d);
2745
- const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
2746
- if (rows.length === 0)
2747
- return [];
2748
- const ids = rows.map((r) => r.config_id);
2749
- return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
2826
+ const normalized = normalizeBoundedReadOptions(options);
2827
+ const total = d.query("SELECT COUNT(*) AS total FROM profile_configs WHERE profile_id = ?").get(profile.id)?.total ?? 0;
2828
+ const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order LIMIT ? OFFSET ?").all(profile.id, normalized.limit, normalized.cursor);
2829
+ return boundedReadPage(rows.map((row) => getConfigById(row.config_id, d)), total, normalized);
2750
2830
  }
2751
2831
  function profileHasSelectors(profile) {
2752
2832
  const selectors = profile.selectors ?? {};
@@ -2762,20 +2842,46 @@ function profileMatchesMachine(profile, machine) {
2762
2842
  const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
2763
2843
  return osMatches && archMatches && hostnameMatches;
2764
2844
  }
2765
- function resolveProfileForMachine(machine = detectMachineContext(), db) {
2766
- const profiles = listProfiles(db).filter(profileHasSelectors);
2767
- const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
2768
- const selectors = profile.selectors;
2769
- const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
2770
- return { profile, score };
2771
- }).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
2772
- return matches[0]?.profile ?? null;
2845
+ function resolveProfileForMachineRead(machine = detectMachineContext(), options = {}, db) {
2846
+ const d = db || getDatabase();
2847
+ const { limit } = normalizeBoundedReadOptions(options);
2848
+ let cursor = 0;
2849
+ let scanned = 0;
2850
+ let total = 0;
2851
+ let selected = null;
2852
+ while (true) {
2853
+ const page = listProfilesPage({ limit, cursor }, d);
2854
+ total = page.total;
2855
+ scanned += page.items.length;
2856
+ for (const profile of page.items) {
2857
+ if (!profileHasSelectors(profile) || !profileMatchesMachine(profile, machine))
2858
+ continue;
2859
+ const selectors = profile.selectors;
2860
+ const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
2861
+ if (!selected || score > selected.score || score === selected.score && profile.name.localeCompare(selected.profile.name) < 0) {
2862
+ selected = { profile, score };
2863
+ }
2864
+ }
2865
+ if (page.complete)
2866
+ break;
2867
+ cursor = page.next_cursor;
2868
+ }
2869
+ return {
2870
+ profile: selected?.profile ?? null,
2871
+ scanned,
2872
+ total,
2873
+ batch_limit: limit,
2874
+ source_bounded: true,
2875
+ complete: true,
2876
+ truncated: false
2877
+ };
2773
2878
  }
2774
2879
  var init_profiles = __esm(() => {
2775
2880
  init_types();
2776
2881
  init_database();
2777
2882
  init_configs();
2778
2883
  init_machine();
2884
+ init_bounded_read();
2779
2885
  });
2780
2886
 
2781
2887
  // src/db/snapshots.ts
@@ -2853,6 +2959,32 @@ var init_machines = __esm(() => {
2853
2959
 
2854
2960
  // src/data/config-store.ts
2855
2961
  import { randomUUID as randomUUID5 } from "crypto";
2962
+ function parseBoundedPagePayload(value, label) {
2963
+ const page = value;
2964
+ const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
2965
+ const complete = Boolean(page && Number.isSafeInteger(page.total) && consumed >= Number(page.total));
2966
+ if (!page || !Array.isArray(page.items) || !Number.isSafeInteger(page.total) || Number(page.total) < 0 || !Number.isSafeInteger(page.limit) || Number(page.limit) < 1 || !Number.isSafeInteger(page.cursor) || Number(page.cursor) < 0 || page.items.length > Number(page.limit) || typeof page.has_more !== "boolean" || typeof page.complete !== "boolean" || page.truncated !== false || page.next_cursor !== null && !Number.isSafeInteger(page.next_cursor) || page.complete !== complete || page.has_more !== !complete || page.next_cursor !== (complete ? null : consumed)) {
2967
+ throw new CloudHttpError(502, `${label} returned an invalid or truncated bounded-read envelope`, value);
2968
+ }
2969
+ return {
2970
+ ...page,
2971
+ source_bounded: page.source_bounded ?? true
2972
+ };
2973
+ }
2974
+ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
2975
+ if (value && typeof value === "object") {
2976
+ const candidate = value;
2977
+ if ("items" in candidate || "total" in candidate || "complete" in candidate || "truncated" in candidate || "next_cursor" in candidate) {
2978
+ return parseBoundedPagePayload(value, label);
2979
+ }
2980
+ }
2981
+ if (!Array.isArray(legacyItems)) {
2982
+ throw new CloudHttpError(502, `${label} returned neither a bounded envelope nor a complete legacy array`, value);
2983
+ }
2984
+ const normalized = normalizeBoundedReadOptions(options);
2985
+ const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
2986
+ return { ...page, source_bounded: false };
2987
+ }
2856
2988
  function isCloudAuthError(err) {
2857
2989
  return err instanceof CloudHttpError && (err.status === 401 || err.status === 403);
2858
2990
  }
@@ -2930,13 +3062,35 @@ class LocalConfigStore {
2930
3062
  return pruneSnapshots(configId, keep, this.db);
2931
3063
  }
2932
3064
  async listProfiles() {
2933
- return listProfiles(this.db);
3065
+ const profiles = [];
3066
+ let cursor = 0;
3067
+ while (true) {
3068
+ const page = await this.listProfilesPage({ limit: 100, cursor });
3069
+ profiles.push(...page.items);
3070
+ if (page.complete)
3071
+ return profiles;
3072
+ cursor = page.next_cursor;
3073
+ }
3074
+ }
3075
+ async listProfilesPage(options = {}) {
3076
+ return listProfilesPage(options, this.db);
2934
3077
  }
2935
3078
  async getProfile(idOrSlug) {
2936
3079
  return getProfile(idOrSlug, this.db);
2937
3080
  }
2938
3081
  async getProfileConfigs(idOrSlug) {
2939
- return getProfileConfigs(idOrSlug, this.db);
3082
+ const configs = [];
3083
+ let cursor = 0;
3084
+ while (true) {
3085
+ const page = await this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor });
3086
+ configs.push(...page.items);
3087
+ if (page.complete)
3088
+ return configs;
3089
+ cursor = page.next_cursor;
3090
+ }
3091
+ }
3092
+ async getProfileConfigsPage(idOrSlug, options = {}) {
3093
+ return getProfileConfigsPage(idOrSlug, options, this.db);
2940
3094
  }
2941
3095
  async createProfile(input) {
2942
3096
  return createProfile(input, this.db);
@@ -2954,7 +3108,10 @@ class LocalConfigStore {
2954
3108
  removeConfigFromProfile(profileIdOrSlug, configId, this.db);
2955
3109
  }
2956
3110
  async resolveProfileForMachine(machine) {
2957
- return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
3111
+ return (await this.resolveProfileForMachineRead(machine)).profile;
3112
+ }
3113
+ async resolveProfileForMachineRead(machine, options = {}) {
3114
+ return machine ? resolveProfileForMachineRead(machine, options, this.db) : resolveProfileForMachineRead(undefined, options, this.db);
2958
3115
  }
2959
3116
  async registerMachine(hostname2, os, arch2) {
2960
3117
  return registerMachine(hostname2, os, arch2, this.db);
@@ -3095,8 +3252,24 @@ class CloudConfigStore {
3095
3252
  return data?.pruned ?? 0;
3096
3253
  }
3097
3254
  async listProfiles() {
3098
- const { data } = await this.request("GET", "/profiles");
3099
- return data?.profiles ?? [];
3255
+ const profiles = [];
3256
+ let cursor = 0;
3257
+ while (true) {
3258
+ const page = await this.listProfilesPage({ limit: 100, cursor });
3259
+ profiles.push(...page.items);
3260
+ if (page.complete)
3261
+ return profiles;
3262
+ cursor = page.next_cursor;
3263
+ }
3264
+ }
3265
+ async listProfilesPage(options = {}) {
3266
+ const normalized = normalizeBoundedReadOptions(options);
3267
+ const params = new URLSearchParams;
3268
+ params.set("limit", String(normalized.limit));
3269
+ params.set("cursor", String(normalized.cursor));
3270
+ const qs = params.toString();
3271
+ const { data } = await this.request("GET", `/profiles${qs ? `?${qs}` : ""}`);
3272
+ return parseBoundedOrLegacyPage(data, data?.profiles, normalized, "profile list");
3100
3273
  }
3101
3274
  async getProfile(idOrSlug) {
3102
3275
  const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
@@ -3106,10 +3279,26 @@ class CloudConfigStore {
3106
3279
  return profile;
3107
3280
  }
3108
3281
  async getProfileConfigs(idOrSlug) {
3109
- const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
3282
+ const configs = [];
3283
+ let cursor = 0;
3284
+ while (true) {
3285
+ const page = await this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor });
3286
+ configs.push(...page.items);
3287
+ if (page.complete)
3288
+ return configs;
3289
+ cursor = page.next_cursor;
3290
+ }
3291
+ }
3292
+ async getProfileConfigsPage(idOrSlug, options = {}) {
3293
+ const normalized = normalizeBoundedReadOptions(options);
3294
+ const params = new URLSearchParams;
3295
+ params.set("limit", String(normalized.limit));
3296
+ params.set("cursor", String(normalized.cursor));
3297
+ const qs = params.toString();
3298
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
3110
3299
  if (status === 404 || !data?.profile)
3111
3300
  throw new ProfileNotFoundError(idOrSlug);
3112
- return data.profile.configs ?? [];
3301
+ return parseBoundedOrLegacyPage(data.configs, data.profile.configs, normalized, "profile membership");
3113
3302
  }
3114
3303
  async createProfile(input) {
3115
3304
  const { data } = await this.request("POST", "/profiles", input, {
@@ -3133,6 +3322,10 @@ class CloudConfigStore {
3133
3322
  await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
3134
3323
  }
3135
3324
  async resolveProfileForMachine(machine) {
3325
+ return (await this.resolveProfileForMachineRead(machine)).profile;
3326
+ }
3327
+ async resolveProfileForMachineRead(machine, options = {}) {
3328
+ const normalized = normalizeBoundedReadOptions(options);
3136
3329
  const params = new URLSearchParams;
3137
3330
  if (machine?.hostname)
3138
3331
  params.set("hostname", machine.hostname);
@@ -3140,11 +3333,40 @@ class CloudConfigStore {
3140
3333
  params.set("os", machine.os);
3141
3334
  if (machine?.arch)
3142
3335
  params.set("arch", machine.arch);
3336
+ params.set("limit", String(normalized.limit));
3143
3337
  const qs = params.toString();
3144
3338
  const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
3145
- if (status === 404 || !data?.profile)
3146
- return null;
3147
- return data.profile;
3339
+ if (status === 404) {
3340
+ return {
3341
+ profile: null,
3342
+ scanned: null,
3343
+ total: null,
3344
+ batch_limit: null,
3345
+ source_bounded: false,
3346
+ complete: true,
3347
+ truncated: false
3348
+ };
3349
+ }
3350
+ if (data && "complete" in data) {
3351
+ if (data.complete !== true || data.truncated !== false) {
3352
+ throw new CloudHttpError(502, "profile resolve returned an incomplete or truncated read", data);
3353
+ }
3354
+ return { ...data, source_bounded: data.source_bounded ?? true };
3355
+ }
3356
+ if (data && "profile" in data) {
3357
+ return {
3358
+ profile: data.profile,
3359
+ scanned: null,
3360
+ total: null,
3361
+ batch_limit: null,
3362
+ source_bounded: false,
3363
+ complete: true,
3364
+ truncated: false
3365
+ };
3366
+ }
3367
+ {
3368
+ throw new CloudHttpError(502, "profile resolve returned an incomplete or truncated read", data);
3369
+ }
3148
3370
  }
3149
3371
  async registerMachine(hostname2, os, arch2) {
3150
3372
  const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
@@ -3181,6 +3403,7 @@ var init_config_store = __esm(() => {
3181
3403
  init_machines();
3182
3404
  init_database();
3183
3405
  init_types();
3406
+ init_bounded_read();
3184
3407
  CloudHttpError = class CloudHttpError extends Error {
3185
3408
  status;
3186
3409
  body;
@@ -13882,6 +14105,7 @@ import { basename as basename7, join as join15, resolve as resolve8 } from "path
13882
14105
 
13883
14106
  // src/lib/config-target-identity.ts
13884
14107
  init_apply();
14108
+ init_database();
13885
14109
  function findConfigsByTargetPath(configs, targetPath) {
13886
14110
  const wanted = normalizeTargetPath(targetPath);
13887
14111
  return configs.filter((config) => {
@@ -13892,6 +14116,20 @@ function findConfigsByTargetPath(configs, targetPath) {
13892
14116
  return normalizeTargetPath(config.target_path) === wanted;
13893
14117
  });
13894
14118
  }
14119
+ function findReferenceConfigsByName(configs, name) {
14120
+ const wantedSlug = slugify(name);
14121
+ return configs.filter((config) => config.kind === "reference" && (config.name === name || slugify(config.name) === wantedSlug));
14122
+ }
14123
+ function findDuplicateReferenceNameGroups(configs) {
14124
+ const groups = new Map;
14125
+ for (const config of configs) {
14126
+ if (config.kind !== "reference")
14127
+ continue;
14128
+ const key = slugify(config.name);
14129
+ groups.set(key, [...groups.get(key) ?? [], config]);
14130
+ }
14131
+ return [...groups.entries()].filter(([, rows]) => rows.length > 1).map(([, rows]) => ({ name: rows[0].name, configs: rows }));
14132
+ }
13895
14133
  function findDuplicateTargetPathGroups(configs) {
13896
14134
  const groups = new Map;
13897
14135
  for (const config of configs) {
@@ -14921,7 +15159,7 @@ var PROJECT_DASHBOARD_PROFILE_VARIABLES = {
14921
15159
  PROJECT_DASHBOARD_DIR: ".hasna/project",
14922
15160
  PROJECT_DASHBOARD_RENDER_MANIFEST: ".hasna/project/dashboard/render.json",
14923
15161
  PROJECT_DASHBOARD_SNAPSHOTS_DIR: ".hasna/project/dashboard/snapshots",
14924
- PROJECT_CHANNEL_PREFIX: "iproj-"
15162
+ PROJECT_CHANNEL_PREFIX: ""
14925
15163
  };
14926
15164
  var PROJECT_DASHBOARD_STANDARD_CONTENT = `# Agent-Managed Project Dashboard Standard
14927
15165
 
@@ -14976,8 +15214,8 @@ context.
14976
15214
 
14977
15215
  ## Coordination
14978
15216
 
14979
- - Project conversation channels use \`iproj-<project-slug>\` in the CLI and are
14980
- displayed to humans as \`#iproj-<project-slug>\`.
15217
+ - Project conversation channels use the normalized project slug in the CLI and
15218
+ are displayed to humans as \`#<project-slug>\`.
14981
15219
  - Todos tasks are the source of truth for work; messages are only coordination.
14982
15220
  - Durable Codewith goal plans should own long-running implementation.
14983
15221
  - New implementation/verification work should route through task-triggered fresh
@@ -15082,10 +15320,15 @@ function mergeProfileSelectors(preset, existing) {
15082
15320
  };
15083
15321
  }
15084
15322
  function mergeProfileVariables(preset, existing) {
15085
- return {
15323
+ const variables = {
15086
15324
  ...preset ?? {},
15087
15325
  ...existing
15088
15326
  };
15327
+ for (const [key, value] of Object.entries(preset ?? {})) {
15328
+ if (value === "")
15329
+ variables[key] = value;
15330
+ }
15331
+ return variables;
15089
15332
  }
15090
15333
  function mergeUnique(preset, existing) {
15091
15334
  const values = [...new Set([...preset ?? [], ...existing ?? []])];
@@ -15388,56 +15631,6 @@ async function getConfigsStatus(store = resolveConfigStore()) {
15388
15631
 
15389
15632
  // src/cli/index.tsx
15390
15633
  init_config_store();
15391
-
15392
- // src/lib/compact-output.ts
15393
- var DEFAULT_LIST_LIMIT = 20;
15394
- var MAX_LIST_LIMIT = 100;
15395
- function parseLimit(value, fallback = DEFAULT_LIST_LIMIT, max = MAX_LIST_LIMIT) {
15396
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
15397
- if (!Number.isFinite(parsed) || parsed <= 0)
15398
- return fallback;
15399
- return Math.min(Math.floor(parsed), max);
15400
- }
15401
- function parseCursor(value) {
15402
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
15403
- if (!Number.isFinite(parsed) || parsed < 0)
15404
- return 0;
15405
- return Math.floor(parsed);
15406
- }
15407
- function paginate(items, opts = {}) {
15408
- const limit = parseLimit(opts.limit, opts.defaultLimit ?? DEFAULT_LIST_LIMIT, opts.maxLimit ?? MAX_LIST_LIMIT);
15409
- const cursor = parseCursor(opts.cursor);
15410
- const pageItems = items.slice(cursor, cursor + limit);
15411
- const nextCursor = cursor + pageItems.length < items.length ? cursor + pageItems.length : null;
15412
- return {
15413
- items: pageItems,
15414
- total: items.length,
15415
- limit,
15416
- cursor,
15417
- next_cursor: nextCursor,
15418
- has_more: nextCursor !== null
15419
- };
15420
- }
15421
- function truncateText(value, max = 80) {
15422
- const text = (value ?? "").replace(/\s+/g, " ").trim();
15423
- if (text.length <= max)
15424
- return text;
15425
- if (max <= 3)
15426
- return text.slice(0, max);
15427
- return `${text.slice(0, max - 3)}...`;
15428
- }
15429
- function truncateMiddle(value, max = 80) {
15430
- const text = (value ?? "").replace(/\s+/g, " ").trim();
15431
- if (text.length <= max)
15432
- return text;
15433
- if (max <= 3)
15434
- return text.slice(0, max);
15435
- const head = Math.ceil((max - 3) * 0.55);
15436
- const tail = Math.floor((max - 3) * 0.45);
15437
- return `${text.slice(0, head)}...${text.slice(text.length - tail)}`;
15438
- }
15439
-
15440
- // src/cli/index.tsx
15441
15634
  import { createRequire } from "module";
15442
15635
  var pkg = createRequire(import.meta.url)("../../package.json");
15443
15636
  var EAGAIN_SLEEP = new Int32Array(new SharedArrayBuffer(4));
@@ -15665,6 +15858,15 @@ function parseVarArgs(values) {
15665
15858
  }
15666
15859
  return Object.keys(vars).length > 0 ? vars : undefined;
15667
15860
  }
15861
+ function parseUnsetVarArgs(values) {
15862
+ if (!values || values.length === 0)
15863
+ return [];
15864
+ const keys = [...new Set(values.map((value) => value.trim()))];
15865
+ if (keys.some((key) => key.length === 0 || key.includes("="))) {
15866
+ throw new Error('Invalid --unset-var (expected variable names without "=")');
15867
+ }
15868
+ return keys;
15869
+ }
15668
15870
  function parseProfileSelectors(opts) {
15669
15871
  const selectors = {};
15670
15872
  const os = splitCsv(opts.os);
@@ -15691,10 +15893,11 @@ function formatProfileSelectorSummary(profile) {
15691
15893
  function formatProfileVariables(profile) {
15692
15894
  return Object.entries(profile.variables).map(([key, value]) => `${key}=${value}`).join(", ");
15693
15895
  }
15694
- async function getMachineProfileContext(opts, store) {
15896
+ async function getMachineProfileContext(opts, store, readOptions = {}) {
15695
15897
  const machine = detectMachineContext({ hostname: opts.hostname, os: opts.os, arch: opts.arch });
15696
- const profile = await store.resolveProfileForMachine(machine);
15697
- return { machine, profile, vars: resolveProfileVariables(profile, machine) };
15898
+ const resolution = await store.resolveProfileForMachineRead(machine, readOptions);
15899
+ const profile = resolution.profile;
15900
+ return { machine, profile, resolution, vars: resolveProfileVariables(profile, machine) };
15698
15901
  }
15699
15902
  program.command("list").alias("ls").description("List stored configs").option("-c, --category <cat>", "filter by category").option("-a, --agent <agent>", "filter by agent").option("-k, --kind <kind>", "filter by kind (file|reference)").option("-t, --tag <tag>", "filter by tag").option("-s, --search <query>", "search name/description/content").option("-f, --format <fmt>", "output format: compact|table|json", "compact").option("--brief", "shorthand for --format compact").option("--verbose", "show expanded metadata for each listed config").option("--json", "output full matching records as JSON").option("--limit <n>", `max rows for human output (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based pagination cursor for human output").action(async (opts) => {
15700
15903
  const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
@@ -15779,20 +15982,33 @@ program.command("add <path>").description("Ingest a file into the config DB").op
15779
15982
  const targetPath = abs.startsWith(homedir7()) ? abs.replace(homedir7(), "~") : abs;
15780
15983
  const name = opts.name || filePath.split("/").pop();
15781
15984
  const store = resolveConfigStore();
15782
- const existingOwners = opts.kind === "reference" ? [] : findConfigsByTargetPath(await store.listConfigs(), targetPath);
15985
+ const allConfigs = await store.listConfigs();
15986
+ const existingOwners = opts.kind === "reference" ? findReferenceConfigsByName(allConfigs, name) : findConfigsByTargetPath(allConfigs, targetPath);
15987
+ const isReference = opts.kind === "reference";
15988
+ const identityLabel = isReference ? `Reference config "${name}"` : targetPath;
15989
+ const identityNoun = isReference ? "name" : "path";
15783
15990
  if (existingOwners.length > 0 && !opts.update) {
15784
15991
  const owners = existingOwners.map((owner) => `${owner.slug} (${owner.id})`).join(", ");
15785
- console.error(chalk.red(`${targetPath} is already tracked by: ${owners}`));
15992
+ console.error(chalk.red(`${identityLabel} is already tracked by: ${owners}`));
15786
15993
  if (existingOwners.length > 1) {
15787
- console.error(chalk.red(` ${existingOwners.length} rows already collide on this path \u2014 apply order between them is undefined.`));
15994
+ console.error(chalk.red(` ${existingOwners.length} rows already collide on this ${identityNoun} \u2014 apply order between them is undefined.`));
15788
15995
  }
15789
15996
  console.error(chalk.dim(" Use `instructions add <path> --update` to refresh that row in place,"));
15790
- console.error(chalk.dim(" `instructions sync` to pull disk changes in, or `instructions delete <id>` first."));
15997
+ if (isReference) {
15998
+ console.error(chalk.dim(" or `instructions delete <id>` first."));
15999
+ } else {
16000
+ console.error(chalk.dim(" `instructions sync` to pull disk changes in, or `instructions delete <id>` first."));
16001
+ }
15791
16002
  process.exit(1);
15792
16003
  }
15793
16004
  let config;
15794
16005
  if (existingOwners.length > 0) {
15795
- const [target, ...rest] = existingOwners;
16006
+ const exactIndex = isReference ? existingOwners.findIndex((owner) => owner.name === name) : -1;
16007
+ const target = exactIndex >= 0 ? existingOwners[exactIndex] : existingOwners[0];
16008
+ const rest = existingOwners.filter((owner) => owner.id !== target.id);
16009
+ if (content !== target.content) {
16010
+ await store.createSnapshot(target.id, target.content, target.version);
16011
+ }
15796
16012
  config = await store.updateConfig(target.id, {
15797
16013
  content,
15798
16014
  format: fmt,
@@ -15802,7 +16018,7 @@ program.command("add <path>").description("Ingest a file into the config DB").op
15802
16018
  });
15803
16019
  console.log(chalk.green("\u2713") + ` Updated: ${chalk.bold(config.name)} ${chalk.dim(`(${config.slug})`)}`);
15804
16020
  if (rest.length > 0) {
15805
- console.log(chalk.yellow(` \u26A0 ${rest.length} other row(s) still target ${targetPath}: ${rest.map((r) => r.slug).join(", ")}`));
16021
+ console.log(chalk.yellow(` \u26A0 ${rest.length} other row(s) still share this ${identityNoun}: ${rest.map((r) => r.slug).join(", ")}`));
15806
16022
  console.log(chalk.yellow(" Apply order between them is undefined. Delete the extras."));
15807
16023
  }
15808
16024
  if (redacted.length > 0) {
@@ -16024,29 +16240,28 @@ program.command("whoami").description("Show setup summary").action(async () => {
16024
16240
  }
16025
16241
  });
16026
16242
  var profileCmd = program.command("profile").description("Manage config profiles (named bundles)");
16027
- profileCmd.command("list").description("List all profiles").option("--brief", "compact one-line output").option("-f, --format <fmt>", "compact|table|json", "compact").option("--verbose", "show expanded profile metadata").option("--json", "output full profiles as JSON").option("--limit <n>", `max rows for human output (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based pagination cursor for human output").action(async (opts) => {
16243
+ profileCmd.command("list").description("List all profiles").option("--brief", "compact one-line output").option("-f, --format <fmt>", "compact|table|json", "compact").option("--verbose", "show expanded profile metadata").option("--json", "output full profiles as JSON").option("--limit <n>", `max rows requested from the source (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based source pagination cursor").action(async (opts) => {
16028
16244
  const fmt = opts.json ? "json" : opts.verbose ? "table" : opts.brief ? "compact" : opts.format;
16029
16245
  const store = resolveConfigStore();
16030
- const profiles = await store.listProfiles();
16246
+ const page = await store.listProfilesPage({ limit: opts.limit, cursor: opts.cursor });
16031
16247
  if (fmt === "json") {
16032
- printJson(profiles);
16248
+ printJson(page);
16033
16249
  return;
16034
16250
  }
16035
- if (profiles.length === 0) {
16251
+ if (page.total === 0) {
16036
16252
  console.log(chalk.dim("No profiles."));
16037
16253
  return;
16038
16254
  }
16039
- const page = paginate(profiles, { limit: opts.limit, cursor: opts.cursor });
16040
16255
  if (fmt === "compact")
16041
16256
  console.log(`${pad("slug", 28)} ${pad("configs", 8)} ${pad("match", 36)} vars`);
16042
16257
  for (const p of page.items) {
16258
+ const configCount = (await store.getProfileConfigsPage(p.id, { limit: 1 })).total;
16043
16259
  if (fmt === "compact") {
16044
16260
  const selectorSummary2 = formatProfileSelectorSummary(p);
16045
- console.log(`${pad(p.slug, 28)} ${pad(String((await store.getProfileConfigs(p.id)).length), 8)} ${pad(selectorSummary2 || "-", 36)} ${Object.keys(p.variables).length}`);
16261
+ console.log(`${pad(p.slug, 28)} ${pad(String(configCount), 8)} ${pad(selectorSummary2 || "-", 36)} ${Object.keys(p.variables).length}`);
16046
16262
  continue;
16047
16263
  }
16048
- const configs = await store.getProfileConfigs(p.id);
16049
- console.log(`${chalk.bold(p.name)} ${chalk.dim(`(${p.slug})`)} \u2014 ${configs.length} config(s)`);
16264
+ console.log(`${chalk.bold(p.name)} ${chalk.dim(`(${p.slug})`)} \u2014 ${configCount} config(s)`);
16050
16265
  if (p.description)
16051
16266
  console.log(` ${chalk.dim(p.description)}`);
16052
16267
  const selectorSummary = formatProfileSelectorSummary(p);
@@ -16067,11 +16282,40 @@ profileCmd.command("create <name>").description("Create a new profile").option("
16067
16282
  });
16068
16283
  console.log(chalk.green("\u2713") + ` Created profile: ${chalk.bold(p.name)} ${chalk.dim(`(${p.slug})`)}`);
16069
16284
  });
16070
- profileCmd.command("show <id>").description("Show profile and its configs").option("--limit <n>", `max config rows (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based pagination cursor").action(async (id, opts) => {
16285
+ profileCmd.command("update <id>").description("Update an existing profile's variables in one store operation").option("--var <vars...>", "set profile variable(s) as KEY=VALUE").option("--unset-var <keys...>", "remove profile variable(s) by key").action(async (id, opts) => {
16286
+ try {
16287
+ const setVariables = parseVarArgs(opts.var) ?? {};
16288
+ const unsetVariables = parseUnsetVarArgs(opts.unsetVar);
16289
+ const setKeys = new Set(Object.keys(setVariables));
16290
+ const conflicts = unsetVariables.filter((key) => setKeys.has(key));
16291
+ if (conflicts.length > 0) {
16292
+ throw new Error(`Variables cannot be both set and unset: ${conflicts.join(", ")}`);
16293
+ }
16294
+ if (Object.keys(setVariables).length === 0 && unsetVariables.length === 0) {
16295
+ throw new Error("Provide --var KEY=VALUE and/or --unset-var KEY");
16296
+ }
16297
+ const store = resolveConfigStore();
16298
+ const profile = await store.getProfile(id);
16299
+ const variables = { ...profile.variables };
16300
+ for (const key of unsetVariables)
16301
+ delete variables[key];
16302
+ Object.assign(variables, setVariables);
16303
+ const updated = await store.updateProfile(profile.id, { variables });
16304
+ console.log(chalk.green("\u2713") + ` Updated profile: ${chalk.bold(updated.name)} ${chalk.dim(`(${updated.slug})`)}`);
16305
+ } catch (e) {
16306
+ console.error(chalk.red(formatCliError(e)));
16307
+ process.exit(1);
16308
+ }
16309
+ });
16310
+ profileCmd.command("show <id>").description("Show profile and its configs").option("--limit <n>", `max config rows (default ${DEFAULT_LIST_LIMIT})`).option("--cursor <n>", "zero-based pagination cursor").option("--json", "output the profile and bounded membership page as JSON").action(async (id, opts) => {
16071
16311
  try {
16072
16312
  const store = resolveConfigStore();
16073
16313
  const p = await store.getProfile(id);
16074
- const configs = await store.getProfileConfigs(id);
16314
+ const page = await store.getProfileConfigsPage(id, { limit: opts.limit, cursor: opts.cursor });
16315
+ if (opts.json) {
16316
+ printJson({ profile: p, configs: page });
16317
+ return;
16318
+ }
16075
16319
  console.log(chalk.bold(p.name) + chalk.dim(` (${p.slug})`));
16076
16320
  if (p.description)
16077
16321
  console.log(chalk.dim(p.description));
@@ -16081,8 +16325,7 @@ profileCmd.command("show <id>").description("Show profile and its configs").opti
16081
16325
  const varSummary = formatProfileVariables(p);
16082
16326
  if (varSummary)
16083
16327
  console.log(chalk.dim(`vars: ${varSummary}`));
16084
- console.log(chalk.cyan(`${configs.length} config(s):`));
16085
- const page = paginate(configs, { limit: opts.limit, cursor: opts.cursor });
16328
+ console.log(chalk.cyan(`${page.total} config(s):`));
16086
16329
  for (const c of page.items)
16087
16330
  console.log(` ${c.slug} ${chalk.dim(`[${c.category}/${c.agent}]`)}`);
16088
16331
  if (page.has_more) {
@@ -16159,9 +16402,15 @@ ${changed}/${results.length} changed (${selected.slug} on ${machine.hostname} ${
16159
16402
  process.exit(1);
16160
16403
  }
16161
16404
  });
16162
- profileCmd.command("resolve").description("Resolve the matching machine-aware profile").option("--hostname <hostname>", "override detected hostname").option("--os <os>", "override detected OS").option("--arch <arch>", "override detected arch").action(async (opts) => {
16405
+ profileCmd.command("resolve").description("Resolve the matching machine-aware profile").option("--hostname <hostname>", "override detected hostname").option("--os <os>", "override detected OS").option("--arch <arch>", "override detected arch").option("--limit <n>", `maximum profiles per source scan batch (default ${DEFAULT_LIST_LIMIT})`).option("--json", "output the complete bounded resolution read as JSON").action(async (opts) => {
16163
16406
  const store = resolveConfigStore();
16164
- const { machine, profile, vars } = await getMachineProfileContext(opts, store);
16407
+ const { machine, profile, resolution, vars } = await getMachineProfileContext(opts, store, { limit: opts.limit });
16408
+ if (opts.json) {
16409
+ printJson({ ...resolution, machine, vars });
16410
+ if (!profile)
16411
+ process.exitCode = 1;
16412
+ return;
16413
+ }
16165
16414
  if (!profile) {
16166
16415
  console.log(chalk.yellow(`No matching profile for ${machine.hostname} ${machine.os_family}/${machine.arch}`));
16167
16416
  process.exit(1);
@@ -16843,6 +17092,20 @@ Stored configs (${allConfigs.length}):`));
16843
17092
  }
16844
17093
  console.log(chalk.dim(" Keep one row per path: `instructions delete <id>` for the extras."));
16845
17094
  }
17095
+ const duplicateReferenceNames = findDuplicateReferenceNameGroups(allConfigs);
17096
+ if (duplicateReferenceNames.length === 0) {
17097
+ pass("No reference config name is claimed by more than one row");
17098
+ } else {
17099
+ const rowCount = duplicateReferenceNames.reduce((total, group) => total + group.configs.length, 0);
17100
+ fail(`${duplicateReferenceNames.length} reference name(s) claimed by more than one row (${rowCount} rows) \u2014 only one is live in the next render`);
17101
+ for (const group of duplicateReferenceNames) {
17102
+ console.log(chalk.yellow(` ${group.name}`));
17103
+ for (const c of group.configs) {
17104
+ console.log(chalk.dim(` ${c.slug} (${c.id}) updated ${c.updated_at}`));
17105
+ }
17106
+ }
17107
+ console.log(chalk.dim(" Keep one row per name: `instructions delete <id>` for the extras."));
17108
+ }
16846
17109
  console.log(`
16847
17110
  ${issues === 0 ? chalk.green("\u2713 All checks passed") : chalk.yellow(`${issues} issue(s) found`)}`);
16848
17111
  });