@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.
- package/README.md +12 -4
- package/dist/cli/add-reference-update.test.d.ts +2 -0
- package/dist/cli/add-reference-update.test.d.ts.map +1 -0
- package/dist/cli/doctor-reference-duplicates.test.d.ts +2 -0
- package/dist/cli/doctor-reference-duplicates.test.d.ts.map +1 -0
- package/dist/cli/index.js +366 -103
- package/dist/cli/profile-reads.test.d.ts +2 -0
- package/dist/cli/profile-reads.test.d.ts.map +1 -0
- package/dist/cli/profile-update.test.d.ts +2 -0
- package/dist/cli/profile-update.test.d.ts.map +1 -0
- package/dist/data/config-store.d.ts +10 -1
- package/dist/data/config-store.d.ts.map +1 -1
- package/dist/db/profiles.d.ts +4 -1
- package/dist/db/profiles.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +226 -30
- package/dist/lib/bounded-read.d.ts +7 -0
- package/dist/lib/bounded-read.d.ts.map +1 -0
- package/dist/lib/config-target-identity.d.ts +76 -0
- package/dist/lib/config-target-identity.d.ts.map +1 -1
- package/dist/lib/project-dashboard-standard.d.ts +1 -1
- package/dist/lib/project-dashboard-standard.d.ts.map +1 -1
- package/dist/mcp/index.js +314 -127
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/server/index.js +246 -36
- package/dist/server/openapi.d.ts +246 -20
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/profile-contract.test.d.ts +2 -0
- package/dist/server/profile-contract.test.d.ts.map +1 -0
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/cloud-store.d.ts +8 -1
- package/dist/storage/cloud-store.d.ts.map +1 -1
- package/dist/types/index.d.ts +24 -0
- package/dist/types/index.d.ts.map +1 -1
- 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;
|
|
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
|
|
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,
|
|
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
|
|
759
|
+
function listProfilesPage(options = {}, db) {
|
|
633
760
|
const d = db || getDatabase();
|
|
634
|
-
|
|
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
|
|
809
|
+
function getProfileConfigsPage(profileIdOrSlug, options = {}, db) {
|
|
680
810
|
const d = db || getDatabase();
|
|
681
811
|
const profile = getProfile(profileIdOrSlug, d);
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
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
|
|
703
|
-
const
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1013
|
-
|
|
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
|
|
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
|
|
1060
|
-
return
|
|
1061
|
-
|
|
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.
|
|
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"],
|
package/dist/mcp/server.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|