@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
@@ -1 +1 @@
1
- {"version":3,"file":"config-target-identity.d.ts","sourceRoot":"","sources":["../../src/lib/config-target-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAGhD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAOvF;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAUlH"}
1
+ {"version":3,"file":"config-target-identity.d.ts","sourceRoot":"","sources":["../../src/lib/config-target-identity.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAIhD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAOvF;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAKpF;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAU9G;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAUlH"}
@@ -2,6 +2,6 @@ import type { Config, ProfileVariables } from "../types/index.js";
2
2
  import { type ConfigStore } from "../data/config-store.js";
3
3
  export declare const PROJECT_DASHBOARD_STANDARD_SLUG = "agent-managed-project-dashboard-standard";
4
4
  export declare const PROJECT_DASHBOARD_PROFILE_VARIABLES: ProfileVariables;
5
- export declare const PROJECT_DASHBOARD_STANDARD_CONTENT = "# Agent-Managed Project Dashboard Standard\n\nThis standard applies to Hasna work projects under `Workspace/<division>/project/<slug>`.\nHumans do not work directly inside these folders; agents keep structure,\nevidence, tasks, knowledge, and dashboard output consistent.\n\n## Canonical Files\n\n- Project manifest root: `.hasna/project/`\n- Dashboard render manifest: `.hasna/project/dashboard/render.json`\n- Latest snapshot: `.hasna/project/dashboard/snapshots/latest.snapshot.json`\n- Dashboard schema ids come from `@hasna/contracts`.\n- Project folders may contain private documents, but render JSON must contain\n only ids, counts, statuses, resource refs, evidence refs, and redacted\n summaries.\n\n## Viewer Commands\n\nUse the Projects-owned viewer. Do not invent a separate per-project app unless\n`open-projects` cannot express the surface.\n\n```bash\nprojects dashboard snapshot <project> --write --json\nprojects dashboard render <project> --json\nprojects dashboard validate <project> --json\nPROJECTS_DASHBOARD_TOKEN=<token> projects dashboard serve <project> --host 0.0.0.0 --port <port>\n```\n\nNon-loopback serving must use `--token`, `PROJECTS_DASHBOARD_TOKEN`, or an\nexplicit `--trust-network` choice. Never put the token in a URL, task\nevidence, render spec, or report.\n\n## Provider Panels\n\nProvider CLIs emit bounded `hasna.project_panel.v1` summaries:\n\n```bash\ntodos project-panel --project <project> --json --contract\nfiles project-panel --project <project> --json --contract\nmailery project-panel --project <project> --limit 20 --json --contract\nconversations project-panel --project <project> --limit 30 --json --contract\nknowledge project-panel --project <project> --scope project --limit 30 --json --contract\nmementos --json project-panel --project <project> --contract\nreports project-panel --project <project> --json --contract\n```\n\nProviders must degrade to unavailable/error panels instead of dumping raw\ncontent. Mailery is workspace-scoped until explicit project-email mapping is\nconfigured; Knowledge must run from the project cwd or with the correct store\ncontext.\n\n## Coordination\n\n- Project conversation channels use `iproj-<project-slug>` in the CLI and are\n displayed to humans as `#iproj-<project-slug>`.\n- Todos tasks are the source of truth for work; messages are only coordination.\n- Durable Codewith goal plans should own long-running implementation.\n- New implementation/verification work should route through task-triggered fresh\n agent runs, not by opening or pasting into existing tmux panes.\n- Agent handoff should reference task ids, project ids, commit ids, evidence\n paths, and dashboard URLs without exposing secrets or raw private documents.\n\n## Report Rules\n\nReports and dashboards should use JSON Render/React Flow through\n`projects dashboard render`. Include decisions, open questions, bank/document\nids, tasks, and evidence refs. Exclude raw email bodies, account numbers, tax\nids, passport numbers, credentials, and contract clauses unless an explicit\napproved storage policy exists.\n";
5
+ export declare const PROJECT_DASHBOARD_STANDARD_CONTENT = "# Agent-Managed Project Dashboard Standard\n\nThis standard applies to Hasna work projects under `Workspace/<division>/project/<slug>`.\nHumans do not work directly inside these folders; agents keep structure,\nevidence, tasks, knowledge, and dashboard output consistent.\n\n## Canonical Files\n\n- Project manifest root: `.hasna/project/`\n- Dashboard render manifest: `.hasna/project/dashboard/render.json`\n- Latest snapshot: `.hasna/project/dashboard/snapshots/latest.snapshot.json`\n- Dashboard schema ids come from `@hasna/contracts`.\n- Project folders may contain private documents, but render JSON must contain\n only ids, counts, statuses, resource refs, evidence refs, and redacted\n summaries.\n\n## Viewer Commands\n\nUse the Projects-owned viewer. Do not invent a separate per-project app unless\n`open-projects` cannot express the surface.\n\n```bash\nprojects dashboard snapshot <project> --write --json\nprojects dashboard render <project> --json\nprojects dashboard validate <project> --json\nPROJECTS_DASHBOARD_TOKEN=<token> projects dashboard serve <project> --host 0.0.0.0 --port <port>\n```\n\nNon-loopback serving must use `--token`, `PROJECTS_DASHBOARD_TOKEN`, or an\nexplicit `--trust-network` choice. Never put the token in a URL, task\nevidence, render spec, or report.\n\n## Provider Panels\n\nProvider CLIs emit bounded `hasna.project_panel.v1` summaries:\n\n```bash\ntodos project-panel --project <project> --json --contract\nfiles project-panel --project <project> --json --contract\nmailery project-panel --project <project> --limit 20 --json --contract\nconversations project-panel --project <project> --limit 30 --json --contract\nknowledge project-panel --project <project> --scope project --limit 30 --json --contract\nmementos --json project-panel --project <project> --contract\nreports project-panel --project <project> --json --contract\n```\n\nProviders must degrade to unavailable/error panels instead of dumping raw\ncontent. Mailery is workspace-scoped until explicit project-email mapping is\nconfigured; Knowledge must run from the project cwd or with the correct store\ncontext.\n\n## Coordination\n\n- Project conversation channels use the normalized project slug in the CLI and\n are displayed to humans as `#<project-slug>`.\n- Todos tasks are the source of truth for work; messages are only coordination.\n- Durable Codewith goal plans should own long-running implementation.\n- New implementation/verification work should route through task-triggered fresh\n agent runs, not by opening or pasting into existing tmux panes.\n- Agent handoff should reference task ids, project ids, commit ids, evidence\n paths, and dashboard URLs without exposing secrets or raw private documents.\n\n## Report Rules\n\nReports and dashboards should use JSON Render/React Flow through\n`projects dashboard render`. Include decisions, open questions, bank/document\nids, tasks, and evidence refs. Exclude raw email bodies, account numbers, tax\nids, passport numbers, credentials, and contract clauses unless an explicit\napproved storage policy exists.\n";
6
6
  export declare function ensureProjectDashboardStandardConfig(store?: ConfigStore): Promise<Config>;
7
7
  //# sourceMappingURL=project-dashboard-standard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"project-dashboard-standard.d.ts","sourceRoot":"","sources":["../../src/lib/project-dashboard-standard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE/E,eAAO,MAAM,+BAA+B,6CAA6C,CAAC;AAE1F,eAAO,MAAM,mCAAmC,EAAE,gBAKjD,CAAC;AAEF,eAAO,MAAM,kCAAkC,2hGAqE9C,CAAC;AAEF,wBAAsB,oCAAoC,CAAC,KAAK,GAAE,WAAkC,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BrH"}
1
+ {"version":3,"file":"project-dashboard-standard.d.ts","sourceRoot":"","sources":["../../src/lib/project-dashboard-standard.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAsB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAE/E,eAAO,MAAM,+BAA+B,6CAA6C,CAAC;AAE1F,eAAO,MAAM,mCAAmC,EAAE,gBAKjD,CAAC;AAEF,eAAO,MAAM,kCAAkC,0hGAqE9C,CAAC;AAEF,wBAAsB,oCAAoC,CAAC,KAAK,GAAE,WAAkC,GAAG,OAAO,CAAC,MAAM,CAAC,CA4BrH"}
package/dist/mcp/index.js CHANGED
@@ -586,6 +586,133 @@ var init_machine = __esm(() => {
586
586
  init_template();
587
587
  });
588
588
 
589
+ // src/lib/compact-output.ts
590
+ function parseLimit(value, fallback = DEFAULT_LIST_LIMIT, max = MAX_LIST_LIMIT) {
591
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
592
+ if (!Number.isFinite(parsed) || parsed <= 0)
593
+ return fallback;
594
+ return Math.min(Math.floor(parsed), max);
595
+ }
596
+ function parseCursor(value) {
597
+ const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
598
+ if (!Number.isFinite(parsed) || parsed < 0)
599
+ return 0;
600
+ return Math.floor(parsed);
601
+ }
602
+ function paginate(items, opts = {}) {
603
+ const limit = parseLimit(opts.limit, opts.defaultLimit ?? DEFAULT_LIST_LIMIT, opts.maxLimit ?? MAX_LIST_LIMIT);
604
+ const cursor = parseCursor(opts.cursor);
605
+ const pageItems = items.slice(cursor, cursor + limit);
606
+ const nextCursor = cursor + pageItems.length < items.length ? cursor + pageItems.length : null;
607
+ return {
608
+ items: pageItems,
609
+ total: items.length,
610
+ limit,
611
+ cursor,
612
+ next_cursor: nextCursor,
613
+ has_more: nextCursor !== null
614
+ };
615
+ }
616
+ function pagedPayload(items, opts = {}) {
617
+ return {
618
+ ...paginate(items, opts),
619
+ hint: opts.hint
620
+ };
621
+ }
622
+ function truncateText(value, max = 80) {
623
+ const text = (value ?? "").replace(/\s+/g, " ").trim();
624
+ if (text.length <= max)
625
+ return text;
626
+ if (max <= 3)
627
+ return text.slice(0, max);
628
+ return `${text.slice(0, max - 3)}...`;
629
+ }
630
+ function summarizeConfig(config, opts = {}) {
631
+ const summary = {
632
+ id: config.id,
633
+ slug: config.slug,
634
+ name: config.name,
635
+ category: config.category,
636
+ agent: config.agent,
637
+ kind: config.kind,
638
+ format: config.format,
639
+ target_path: config.target_path,
640
+ output_count: config.outputs.length,
641
+ version: config.version,
642
+ is_template: config.is_template
643
+ };
644
+ if (opts.verbose) {
645
+ summary.updated_at = config.updated_at;
646
+ summary.description = config.description;
647
+ summary.tags = config.tags;
648
+ summary.outputs = config.outputs;
649
+ }
650
+ return summary;
651
+ }
652
+ function summarizeProfile(profile, opts = {}) {
653
+ const selectorCount = (profile.selectors.os?.length ?? 0) + (profile.selectors.arch?.length ?? 0) + (profile.selectors.hostnames?.length ?? 0);
654
+ const summary = {
655
+ id: profile.id,
656
+ slug: profile.slug,
657
+ name: profile.name,
658
+ description: opts.verbose ? profile.description : truncateText(profile.description, 80),
659
+ selector_count: selectorCount,
660
+ variable_count: Object.keys(profile.variables).length
661
+ };
662
+ if (opts.verbose) {
663
+ summary.created_at = profile.created_at;
664
+ summary.updated_at = profile.updated_at;
665
+ summary.selectors = profile.selectors;
666
+ summary.variables = profile.variables;
667
+ }
668
+ return summary;
669
+ }
670
+ function summarizeApplyResult(result) {
671
+ return {
672
+ config_id: result.config_id,
673
+ path: result.path,
674
+ dry_run: result.dry_run,
675
+ changed: result.changed,
676
+ primary_changed: result.primary_changed,
677
+ agent: result.agent,
678
+ transform: result.transform,
679
+ output_count: result.outputs?.length ?? 0,
680
+ outputs: result.outputs?.map(summarizeApplyResult)
681
+ };
682
+ }
683
+ var DEFAULT_LIST_LIMIT = 20, MAX_LIST_LIMIT = 100;
684
+
685
+ // src/lib/bounded-read.ts
686
+ function normalizeBoundedReadOptions(options = {}) {
687
+ return {
688
+ limit: parseLimit(options.limit, DEFAULT_LIST_LIMIT, MAX_LIST_LIMIT),
689
+ cursor: parseCursor(options.cursor)
690
+ };
691
+ }
692
+ function boundedReadPage(items, total, options = {}) {
693
+ const { limit, cursor } = normalizeBoundedReadOptions(options);
694
+ if (items.length > limit) {
695
+ throw new Error(`bounded read returned ${items.length} rows for limit ${limit}`);
696
+ }
697
+ const consumed = cursor + items.length;
698
+ const complete = consumed >= total;
699
+ if (!complete && items.length === 0) {
700
+ throw new Error(`bounded read did not advance at cursor ${cursor} of ${total}`);
701
+ }
702
+ return {
703
+ items,
704
+ total,
705
+ limit,
706
+ cursor,
707
+ next_cursor: complete ? null : consumed,
708
+ has_more: !complete,
709
+ complete,
710
+ truncated: false,
711
+ source_bounded: true
712
+ };
713
+ }
714
+ var init_bounded_read = () => {};
715
+
589
716
  // src/db/profiles.ts
590
717
  function rowToProfile(row) {
591
718
  return {
@@ -629,9 +756,12 @@ function getProfile(idOrSlug, db) {
629
756
  throw new ProfileNotFoundError(idOrSlug);
630
757
  return rowToProfile(row);
631
758
  }
632
- function listProfiles(db) {
759
+ function listProfilesPage(options = {}, db) {
633
760
  const d = db || getDatabase();
634
- return d.query("SELECT * FROM profiles ORDER BY name").all().map(rowToProfile);
761
+ const normalized = normalizeBoundedReadOptions(options);
762
+ const total = d.query("SELECT COUNT(*) AS total FROM profiles").get()?.total ?? 0;
763
+ const rows = d.query("SELECT * FROM profiles ORDER BY name LIMIT ? OFFSET ?").all(normalized.limit, normalized.cursor).map(rowToProfile);
764
+ return boundedReadPage(rows, total, normalized);
635
765
  }
636
766
  function updateProfile(idOrSlug, input, db) {
637
767
  const d = db || getDatabase();
@@ -676,14 +806,13 @@ function removeConfigFromProfile(profileIdOrSlug, configId, db) {
676
806
  const profile = getProfile(profileIdOrSlug, d);
677
807
  d.run("DELETE FROM profile_configs WHERE profile_id = ? AND config_id = ?", [profile.id, configId]);
678
808
  }
679
- function getProfileConfigs(profileIdOrSlug, db) {
809
+ function getProfileConfigsPage(profileIdOrSlug, options = {}, db) {
680
810
  const d = db || getDatabase();
681
811
  const profile = getProfile(profileIdOrSlug, d);
682
- const rows = d.query("SELECT config_id FROM profile_configs WHERE profile_id = ? ORDER BY sort_order").all(profile.id);
683
- if (rows.length === 0)
684
- return [];
685
- const ids = rows.map((r) => r.config_id);
686
- return listConfigs(undefined, d).filter((c) => ids.includes(c.id));
812
+ const normalized = normalizeBoundedReadOptions(options);
813
+ const total = d.query("SELECT COUNT(*) AS total FROM profile_configs WHERE profile_id = ?").get(profile.id)?.total ?? 0;
814
+ 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);
815
+ return boundedReadPage(rows.map((row) => getConfigById(row.config_id, d)), total, normalized);
687
816
  }
688
817
  function profileHasSelectors(profile) {
689
818
  const selectors = profile.selectors ?? {};
@@ -699,20 +828,46 @@ function profileMatchesMachine(profile, machine) {
699
828
  const hostnameMatches = !selectors.hostnames?.length || selectors.hostnames.some((candidate) => candidate.trim().toLowerCase() === machine.hostname.trim().toLowerCase());
700
829
  return osMatches && archMatches && hostnameMatches;
701
830
  }
702
- function resolveProfileForMachine(machine = detectMachineContext(), db) {
703
- const profiles = listProfiles(db).filter(profileHasSelectors);
704
- const matches = profiles.filter((profile) => profileMatchesMachine(profile, machine)).map((profile) => {
705
- const selectors = profile.selectors;
706
- const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
707
- return { profile, score };
708
- }).sort((a, b) => b.score - a.score || a.profile.name.localeCompare(b.profile.name));
709
- return matches[0]?.profile ?? null;
831
+ function resolveProfileForMachineRead(machine = detectMachineContext(), options = {}, db) {
832
+ const d = db || getDatabase();
833
+ const { limit } = normalizeBoundedReadOptions(options);
834
+ let cursor = 0;
835
+ let scanned = 0;
836
+ let total = 0;
837
+ let selected = null;
838
+ while (true) {
839
+ const page = listProfilesPage({ limit, cursor }, d);
840
+ total = page.total;
841
+ scanned += page.items.length;
842
+ for (const profile of page.items) {
843
+ if (!profileHasSelectors(profile) || !profileMatchesMachine(profile, machine))
844
+ continue;
845
+ const selectors = profile.selectors;
846
+ const score = (selectors.hostnames?.length ? 100 : 0) + (selectors.os?.length ? 10 : 0) + (selectors.arch?.length ? 10 : 0);
847
+ if (!selected || score > selected.score || score === selected.score && profile.name.localeCompare(selected.profile.name) < 0) {
848
+ selected = { profile, score };
849
+ }
850
+ }
851
+ if (page.complete)
852
+ break;
853
+ cursor = page.next_cursor;
854
+ }
855
+ return {
856
+ profile: selected?.profile ?? null,
857
+ scanned,
858
+ total,
859
+ batch_limit: limit,
860
+ source_bounded: true,
861
+ complete: true,
862
+ truncated: false
863
+ };
710
864
  }
711
865
  var init_profiles = __esm(() => {
712
866
  init_types();
713
867
  init_database();
714
868
  init_configs();
715
869
  init_machine();
870
+ init_bounded_read();
716
871
  });
717
872
 
718
873
  // src/db/snapshots.ts
@@ -790,6 +945,32 @@ var init_machines = __esm(() => {
790
945
 
791
946
  // src/data/config-store.ts
792
947
  import { randomUUID as randomUUID2 } from "crypto";
948
+ function parseBoundedPagePayload(value, label) {
949
+ const page = value;
950
+ const consumed = Number(page?.cursor) + (page?.items?.length ?? 0);
951
+ const complete = Boolean(page && Number.isSafeInteger(page.total) && consumed >= Number(page.total));
952
+ 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)) {
953
+ throw new CloudHttpError(502, `${label} returned an invalid or truncated bounded-read envelope`, value);
954
+ }
955
+ return {
956
+ ...page,
957
+ source_bounded: page.source_bounded ?? true
958
+ };
959
+ }
960
+ function parseBoundedOrLegacyPage(value, legacyItems, options, label) {
961
+ if (value && typeof value === "object") {
962
+ const candidate = value;
963
+ if ("items" in candidate || "total" in candidate || "complete" in candidate || "truncated" in candidate || "next_cursor" in candidate) {
964
+ return parseBoundedPagePayload(value, label);
965
+ }
966
+ }
967
+ if (!Array.isArray(legacyItems)) {
968
+ throw new CloudHttpError(502, `${label} returned neither a bounded envelope nor a complete legacy array`, value);
969
+ }
970
+ const normalized = normalizeBoundedReadOptions(options);
971
+ const page = boundedReadPage(legacyItems.slice(normalized.cursor, normalized.cursor + normalized.limit), legacyItems.length, normalized);
972
+ return { ...page, source_bounded: false };
973
+ }
793
974
  function resolveCloudConfig(env = process.env) {
794
975
  const apiUrl = env[API_URL_ENV]?.trim();
795
976
  const apiKey = env[API_KEY_ENV]?.trim();
@@ -844,13 +1025,35 @@ class LocalConfigStore {
844
1025
  return pruneSnapshots(configId, keep, this.db);
845
1026
  }
846
1027
  async listProfiles() {
847
- return listProfiles(this.db);
1028
+ const profiles = [];
1029
+ let cursor = 0;
1030
+ while (true) {
1031
+ const page = await this.listProfilesPage({ limit: 100, cursor });
1032
+ profiles.push(...page.items);
1033
+ if (page.complete)
1034
+ return profiles;
1035
+ cursor = page.next_cursor;
1036
+ }
1037
+ }
1038
+ async listProfilesPage(options = {}) {
1039
+ return listProfilesPage(options, this.db);
848
1040
  }
849
1041
  async getProfile(idOrSlug) {
850
1042
  return getProfile(idOrSlug, this.db);
851
1043
  }
852
1044
  async getProfileConfigs(idOrSlug) {
853
- return getProfileConfigs(idOrSlug, this.db);
1045
+ const configs = [];
1046
+ let cursor = 0;
1047
+ while (true) {
1048
+ const page = await this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor });
1049
+ configs.push(...page.items);
1050
+ if (page.complete)
1051
+ return configs;
1052
+ cursor = page.next_cursor;
1053
+ }
1054
+ }
1055
+ async getProfileConfigsPage(idOrSlug, options = {}) {
1056
+ return getProfileConfigsPage(idOrSlug, options, this.db);
854
1057
  }
855
1058
  async createProfile(input) {
856
1059
  return createProfile(input, this.db);
@@ -868,7 +1071,10 @@ class LocalConfigStore {
868
1071
  removeConfigFromProfile(profileIdOrSlug, configId, this.db);
869
1072
  }
870
1073
  async resolveProfileForMachine(machine) {
871
- return machine ? resolveProfileForMachine(machine, this.db) : resolveProfileForMachine(undefined, this.db);
1074
+ return (await this.resolveProfileForMachineRead(machine)).profile;
1075
+ }
1076
+ async resolveProfileForMachineRead(machine, options = {}) {
1077
+ return machine ? resolveProfileForMachineRead(machine, options, this.db) : resolveProfileForMachineRead(undefined, options, this.db);
872
1078
  }
873
1079
  async registerMachine(hostname2, os, arch2) {
874
1080
  return registerMachine(hostname2, os, arch2, this.db);
@@ -1009,8 +1215,24 @@ class CloudConfigStore {
1009
1215
  return data?.pruned ?? 0;
1010
1216
  }
1011
1217
  async listProfiles() {
1012
- const { data } = await this.request("GET", "/profiles");
1013
- return data?.profiles ?? [];
1218
+ const profiles = [];
1219
+ let cursor = 0;
1220
+ while (true) {
1221
+ const page = await this.listProfilesPage({ limit: 100, cursor });
1222
+ profiles.push(...page.items);
1223
+ if (page.complete)
1224
+ return profiles;
1225
+ cursor = page.next_cursor;
1226
+ }
1227
+ }
1228
+ async listProfilesPage(options = {}) {
1229
+ const normalized = normalizeBoundedReadOptions(options);
1230
+ const params = new URLSearchParams;
1231
+ params.set("limit", String(normalized.limit));
1232
+ params.set("cursor", String(normalized.cursor));
1233
+ const qs = params.toString();
1234
+ const { data } = await this.request("GET", `/profiles${qs ? `?${qs}` : ""}`);
1235
+ return parseBoundedOrLegacyPage(data, data?.profiles, normalized, "profile list");
1014
1236
  }
1015
1237
  async getProfile(idOrSlug) {
1016
1238
  const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
@@ -1020,10 +1242,26 @@ class CloudConfigStore {
1020
1242
  return profile;
1021
1243
  }
1022
1244
  async getProfileConfigs(idOrSlug) {
1023
- const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}`, undefined, { allow404: true });
1245
+ const configs = [];
1246
+ let cursor = 0;
1247
+ while (true) {
1248
+ const page = await this.getProfileConfigsPage(idOrSlug, { limit: 100, cursor });
1249
+ configs.push(...page.items);
1250
+ if (page.complete)
1251
+ return configs;
1252
+ cursor = page.next_cursor;
1253
+ }
1254
+ }
1255
+ async getProfileConfigsPage(idOrSlug, options = {}) {
1256
+ const normalized = normalizeBoundedReadOptions(options);
1257
+ const params = new URLSearchParams;
1258
+ params.set("limit", String(normalized.limit));
1259
+ params.set("cursor", String(normalized.cursor));
1260
+ const qs = params.toString();
1261
+ const { status, data } = await this.request("GET", `/profiles/${encodeURIComponent(idOrSlug)}${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
1024
1262
  if (status === 404 || !data?.profile)
1025
1263
  throw new ProfileNotFoundError(idOrSlug);
1026
- return data.profile.configs ?? [];
1264
+ return parseBoundedOrLegacyPage(data.configs, data.profile.configs, normalized, "profile membership");
1027
1265
  }
1028
1266
  async createProfile(input) {
1029
1267
  const { data } = await this.request("POST", "/profiles", input, {
@@ -1047,6 +1285,10 @@ class CloudConfigStore {
1047
1285
  await this.request("DELETE", `/profiles/${encodeURIComponent(profileIdOrSlug)}/configs/${encodeURIComponent(configId)}`, undefined, { allow404: true });
1048
1286
  }
1049
1287
  async resolveProfileForMachine(machine) {
1288
+ return (await this.resolveProfileForMachineRead(machine)).profile;
1289
+ }
1290
+ async resolveProfileForMachineRead(machine, options = {}) {
1291
+ const normalized = normalizeBoundedReadOptions(options);
1050
1292
  const params = new URLSearchParams;
1051
1293
  if (machine?.hostname)
1052
1294
  params.set("hostname", machine.hostname);
@@ -1054,11 +1296,40 @@ class CloudConfigStore {
1054
1296
  params.set("os", machine.os);
1055
1297
  if (machine?.arch)
1056
1298
  params.set("arch", machine.arch);
1299
+ params.set("limit", String(normalized.limit));
1057
1300
  const qs = params.toString();
1058
1301
  const { status, data } = await this.request("GET", `/profiles/resolve${qs ? `?${qs}` : ""}`, undefined, { allow404: true });
1059
- if (status === 404 || !data?.profile)
1060
- return null;
1061
- return data.profile;
1302
+ if (status === 404) {
1303
+ return {
1304
+ profile: null,
1305
+ scanned: null,
1306
+ total: null,
1307
+ batch_limit: null,
1308
+ source_bounded: false,
1309
+ complete: true,
1310
+ truncated: false
1311
+ };
1312
+ }
1313
+ if (data && "complete" in data) {
1314
+ if (data.complete !== true || data.truncated !== false) {
1315
+ throw new CloudHttpError(502, "profile resolve returned an incomplete or truncated read", data);
1316
+ }
1317
+ return { ...data, source_bounded: data.source_bounded ?? true };
1318
+ }
1319
+ if (data && "profile" in data) {
1320
+ return {
1321
+ profile: data.profile,
1322
+ scanned: null,
1323
+ total: null,
1324
+ batch_limit: null,
1325
+ source_bounded: false,
1326
+ complete: true,
1327
+ truncated: false
1328
+ };
1329
+ }
1330
+ {
1331
+ throw new CloudHttpError(502, "profile resolve returned an incomplete or truncated read", data);
1332
+ }
1062
1333
  }
1063
1334
  async registerMachine(hostname2, os, arch2) {
1064
1335
  const { data } = await this.request("POST", "/machines", { hostname: hostname2, os, arch: arch2 }, { idempotent: true });
@@ -1095,6 +1366,7 @@ var init_config_store = __esm(() => {
1095
1366
  init_machines();
1096
1367
  init_database();
1097
1368
  init_types();
1369
+ init_bounded_read();
1098
1370
  CloudHttpError = class CloudHttpError extends Error {
1099
1371
  status;
1100
1372
  body;
@@ -6957,7 +7229,7 @@ var init_sync_dir = __esm(() => {
6957
7229
  var require_package = __commonJS((exports, module) => {
6958
7230
  module.exports = {
6959
7231
  name: "@hasna/instructions",
6960
- version: "0.4.19",
7232
+ version: "0.4.21",
6961
7233
  description: "AI coding agent instruction & configuration manager \u2014 store, version, apply, and share all your AI coding configs. CLI + MCP + HTTP API (instructions-serve) + generated SDK + Dashboard.",
6962
7234
  type: "module",
6963
7235
  main: "dist/index.js",
@@ -7062,6 +7334,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
7062
7334
 
7063
7335
  // src/lib/config-target-identity.ts
7064
7336
  init_apply();
7337
+ init_database();
7065
7338
  function findConfigsByTargetPath(configs, targetPath) {
7066
7339
  const wanted = normalizeTargetPath(targetPath);
7067
7340
  return configs.filter((config) => {
@@ -7072,113 +7345,18 @@ function findConfigsByTargetPath(configs, targetPath) {
7072
7345
  return normalizeTargetPath(config.target_path) === wanted;
7073
7346
  });
7074
7347
  }
7348
+ function findReferenceConfigsByName(configs, name) {
7349
+ const wantedSlug = slugify(name);
7350
+ return configs.filter((config) => config.kind === "reference" && (config.name === name || slugify(config.name) === wantedSlug));
7351
+ }
7075
7352
 
7076
7353
  // src/mcp/server.ts
7077
7354
  init_sync_dir();
7078
7355
  init_machine();
7079
-
7080
- // src/lib/compact-output.ts
7081
- var DEFAULT_LIST_LIMIT = 20;
7082
- var MAX_LIST_LIMIT = 100;
7083
- function parseLimit(value, fallback = DEFAULT_LIST_LIMIT, max = MAX_LIST_LIMIT) {
7084
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
7085
- if (!Number.isFinite(parsed) || parsed <= 0)
7086
- return fallback;
7087
- return Math.min(Math.floor(parsed), max);
7088
- }
7089
- function parseCursor(value) {
7090
- const parsed = typeof value === "number" ? value : Number.parseInt(String(value ?? ""), 10);
7091
- if (!Number.isFinite(parsed) || parsed < 0)
7092
- return 0;
7093
- return Math.floor(parsed);
7094
- }
7095
- function paginate(items, opts = {}) {
7096
- const limit = parseLimit(opts.limit, opts.defaultLimit ?? DEFAULT_LIST_LIMIT, opts.maxLimit ?? MAX_LIST_LIMIT);
7097
- const cursor = parseCursor(opts.cursor);
7098
- const pageItems = items.slice(cursor, cursor + limit);
7099
- const nextCursor = cursor + pageItems.length < items.length ? cursor + pageItems.length : null;
7100
- return {
7101
- items: pageItems,
7102
- total: items.length,
7103
- limit,
7104
- cursor,
7105
- next_cursor: nextCursor,
7106
- has_more: nextCursor !== null
7107
- };
7108
- }
7109
- function pagedPayload(items, opts = {}) {
7110
- return {
7111
- ...paginate(items, opts),
7112
- hint: opts.hint
7113
- };
7114
- }
7115
- function truncateText(value, max = 80) {
7116
- const text = (value ?? "").replace(/\s+/g, " ").trim();
7117
- if (text.length <= max)
7118
- return text;
7119
- if (max <= 3)
7120
- return text.slice(0, max);
7121
- return `${text.slice(0, max - 3)}...`;
7122
- }
7123
- function summarizeConfig(config, opts = {}) {
7124
- const summary = {
7125
- id: config.id,
7126
- slug: config.slug,
7127
- name: config.name,
7128
- category: config.category,
7129
- agent: config.agent,
7130
- kind: config.kind,
7131
- format: config.format,
7132
- target_path: config.target_path,
7133
- output_count: config.outputs.length,
7134
- version: config.version,
7135
- is_template: config.is_template
7136
- };
7137
- if (opts.verbose) {
7138
- summary.updated_at = config.updated_at;
7139
- summary.description = config.description;
7140
- summary.tags = config.tags;
7141
- summary.outputs = config.outputs;
7142
- }
7143
- return summary;
7144
- }
7145
- function summarizeProfile(profile, opts = {}) {
7146
- const selectorCount = (profile.selectors.os?.length ?? 0) + (profile.selectors.arch?.length ?? 0) + (profile.selectors.hostnames?.length ?? 0);
7147
- const summary = {
7148
- id: profile.id,
7149
- slug: profile.slug,
7150
- name: profile.name,
7151
- description: opts.verbose ? profile.description : truncateText(profile.description, 80),
7152
- selector_count: selectorCount,
7153
- variable_count: Object.keys(profile.variables).length
7154
- };
7155
- if (opts.verbose) {
7156
- summary.created_at = profile.created_at;
7157
- summary.updated_at = profile.updated_at;
7158
- summary.selectors = profile.selectors;
7159
- summary.variables = profile.variables;
7160
- }
7161
- return summary;
7162
- }
7163
- function summarizeApplyResult(result) {
7164
- return {
7165
- config_id: result.config_id,
7166
- path: result.path,
7167
- dry_run: result.dry_run,
7168
- changed: result.changed,
7169
- primary_changed: result.primary_changed,
7170
- agent: result.agent,
7171
- transform: result.transform,
7172
- output_count: result.outputs?.length ?? 0,
7173
- outputs: result.outputs?.map(summarizeApplyResult)
7174
- };
7175
- }
7176
-
7177
- // src/mcp/server.ts
7178
7356
  var TOOL_DOCS = {
7179
7357
  list_configs: "List configs. Params: category?, agent?, kind?, search?, limit?, cursor?, verbose?. Defaults to a paged compact envelope without content; use get_config for full content.",
7180
7358
  get_config: "Get a config by id or slug. Returns full config including content.",
7181
- create_config: "Create a new config. Required: name, content, category. Optional: agent, target_path, outputs, kind, format, tags, description, is_template. Refuses when target_path is already tracked by another config (one target path, one row) \u2014 use update_config on the owning row, or delete_config first. kind:'reference' owns no target path and is exempt.",
7359
+ create_config: "Create a new config. Required: name, content, category. Optional: agent, target_path, outputs, kind, format, tags, description, is_template. Refuses when target_path is already tracked by another config (one target path, one row) \u2014 use update_config on the owning row, or delete_config first. kind:'reference' owns no target path and is exempt from that check, but is instead refused when its name (or a case/punctuation variant of it) already tracks another reference config.",
7182
7360
  update_config: "Update a config by id or slug. Optional: content, name, tags, description, category, agent, target_path, outputs.",
7183
7361
  apply_config: "Apply a config through the shared ownership gate. Params: id_or_slug, dry_run?, verbose?. Returns results plus session-renderer-owned targets that were skipped.",
7184
7362
  sync_directory: "Sync a directory with the DB. Params: dir, direction ('from_disk'|'to_disk'). Returns sync result.",
@@ -7269,6 +7447,15 @@ function buildServer() {
7269
7447
  return err(`${targetPath} is already tracked by: ${named}.${collision}` + ` Use update_config on that row to refresh it in place, or delete_config first.`);
7270
7448
  }
7271
7449
  }
7450
+ if (kind === "reference") {
7451
+ const name2 = args["name"];
7452
+ const owners = findReferenceConfigsByName(await store.listConfigs(), name2);
7453
+ if (owners.length > 0) {
7454
+ const named = owners.map((owner) => `${owner.slug} (${owner.id})`).join(", ");
7455
+ const collision = owners.length > 1 ? ` ${owners.length} rows already collide on this name \u2014 apply order between them is undefined.` : "";
7456
+ return err(`Reference config "${name2}" is already tracked by: ${named}.${collision}` + ` Use update_config on that row to refresh it in place, or delete_config first.`);
7457
+ }
7458
+ }
7272
7459
  const c = await store.createConfig({
7273
7460
  name: args["name"],
7274
7461
  content: args["content"],
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AA4EnE,wBAAgB,WAAW,IAAI,MAAM,CAyTpC"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AA4EnE,wBAAgB,WAAW,IAAI,MAAM,CAyVpC"}