aidimag 1.0.0

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 (91) hide show
  1. package/LICENSE +102 -0
  2. package/README.md +113 -0
  3. package/dist/capture/bootstrap.d.ts +25 -0
  4. package/dist/capture/bootstrap.js +188 -0
  5. package/dist/capture/commit-miner.d.ts +59 -0
  6. package/dist/capture/commit-miner.js +381 -0
  7. package/dist/capture/harvest.d.ts +50 -0
  8. package/dist/capture/harvest.js +207 -0
  9. package/dist/capture/pr-miner.d.ts +57 -0
  10. package/dist/capture/pr-miner.js +185 -0
  11. package/dist/capture/session-briefing.d.ts +36 -0
  12. package/dist/capture/session-briefing.js +150 -0
  13. package/dist/capture/session-extraction.d.ts +23 -0
  14. package/dist/capture/session-extraction.js +54 -0
  15. package/dist/capture/triage.d.ts +30 -0
  16. package/dist/capture/triage.js +108 -0
  17. package/dist/cli/commands/capture.d.ts +5 -0
  18. package/dist/cli/commands/capture.js +357 -0
  19. package/dist/cli/commands/hosts.d.ts +5 -0
  20. package/dist/cli/commands/hosts.js +98 -0
  21. package/dist/cli/commands/knowledge.d.ts +5 -0
  22. package/dist/cli/commands/knowledge.js +121 -0
  23. package/dist/cli/commands/memory.d.ts +6 -0
  24. package/dist/cli/commands/memory.js +392 -0
  25. package/dist/cli/commands/sync.d.ts +5 -0
  26. package/dist/cli/commands/sync.js +307 -0
  27. package/dist/cli/commands/tickets.d.ts +6 -0
  28. package/dist/cli/commands/tickets.js +328 -0
  29. package/dist/cli/commands/verify.d.ts +5 -0
  30. package/dist/cli/commands/verify.js +136 -0
  31. package/dist/cli/index.d.ts +19 -0
  32. package/dist/cli/index.js +46 -0
  33. package/dist/cli/shared.d.ts +37 -0
  34. package/dist/cli/shared.js +175 -0
  35. package/dist/config.d.ts +55 -0
  36. package/dist/config.js +51 -0
  37. package/dist/context/generate.d.ts +24 -0
  38. package/dist/context/generate.js +115 -0
  39. package/dist/critique/critique.d.ts +40 -0
  40. package/dist/critique/critique.js +110 -0
  41. package/dist/db/schema.d.ts +28 -0
  42. package/dist/db/schema.js +269 -0
  43. package/dist/db/store.d.ts +182 -0
  44. package/dist/db/store.js +906 -0
  45. package/dist/debug.d.ts +14 -0
  46. package/dist/debug.js +25 -0
  47. package/dist/embeddings/provider.d.ts +16 -0
  48. package/dist/embeddings/provider.js +100 -0
  49. package/dist/embeddings/search.d.ts +22 -0
  50. package/dist/embeddings/search.js +95 -0
  51. package/dist/index.d.ts +11 -0
  52. package/dist/index.js +12 -0
  53. package/dist/knowledge/chunk.d.ts +9 -0
  54. package/dist/knowledge/chunk.js +78 -0
  55. package/dist/knowledge/extract.d.ts +34 -0
  56. package/dist/knowledge/extract.js +113 -0
  57. package/dist/knowledge/ingest.d.ts +79 -0
  58. package/dist/knowledge/ingest.js +305 -0
  59. package/dist/knowledge/llm.d.ts +21 -0
  60. package/dist/knowledge/llm.js +110 -0
  61. package/dist/mcp/server.d.ts +11 -0
  62. package/dist/mcp/server.js +532 -0
  63. package/dist/security/evidence.d.ts +11 -0
  64. package/dist/security/evidence.js +15 -0
  65. package/dist/security/seal.d.ts +3 -0
  66. package/dist/security/seal.js +28 -0
  67. package/dist/security/sync-push.d.ts +2 -0
  68. package/dist/security/sync-push.js +37 -0
  69. package/dist/security/url.d.ts +6 -0
  70. package/dist/security/url.js +67 -0
  71. package/dist/sync/client.d.ts +84 -0
  72. package/dist/sync/client.js +391 -0
  73. package/dist/sync/server.d.ts +75 -0
  74. package/dist/sync/server.js +659 -0
  75. package/dist/tickets/provider.d.ts +80 -0
  76. package/dist/tickets/provider.js +375 -0
  77. package/dist/types.d.ts +133 -0
  78. package/dist/types.js +5 -0
  79. package/dist/ui/page.d.ts +5 -0
  80. package/dist/ui/page.js +841 -0
  81. package/dist/ui/server.d.ts +10 -0
  82. package/dist/ui/server.js +437 -0
  83. package/dist/verify/check.d.ts +38 -0
  84. package/dist/verify/check.js +121 -0
  85. package/dist/verify/engine.d.ts +39 -0
  86. package/dist/verify/engine.js +178 -0
  87. package/dist/verify/hooks.d.ts +24 -0
  88. package/dist/verify/hooks.js +125 -0
  89. package/dist/verify/runners.d.ts +26 -0
  90. package/dist/verify/runners.js +193 -0
  91. package/package.json +78 -0
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Team-sync commands: serve, cloud, login, logout, sync, keys.
3
+ */
4
+ import { MemoryStore, findRepoRoot } from "../../db/store.js";
5
+ import { fail, maybeRegenerateContext, openBrowser, createPrompter } from "../shared.js";
6
+ export function registerSyncCommands(program) {
7
+ program
8
+ .command("serve")
9
+ .description("Run a self-hosted team sync server")
10
+ .option("-p, --port <n>", "Port", "8787")
11
+ .option("-d, --db <path>", "Server database file", "./aidimag-sync.db")
12
+ .option("-t, --token <token>", "Shared auth token (or AIDIMAG_SYNC_TOKEN env)")
13
+ .action(async (opts) => {
14
+ const token = opts.token ?? process.env.AIDIMAG_SYNC_TOKEN;
15
+ if (!token)
16
+ fail("provide --token or set AIDIMAG_SYNC_TOKEN");
17
+ const { startSyncServer } = await import("../../sync/server.js");
18
+ const url = await startSyncServer({ dbPath: opts.db, token, port: parseInt(opts.port, 10) });
19
+ console.log(`aidimag sync server: ${url} (db: ${opts.db}, Ctrl+C to stop)`);
20
+ console.log(`Link a repo with: dim cloud link --server ${url} --brain <name> --token <token>`);
21
+ });
22
+ program
23
+ .command("cloud")
24
+ .description("Manage the repo's cloud/team-sync binding")
25
+ .argument("<action>", "link | unlink | status | remote")
26
+ .option("-s, --server <url>", "Sync server URL")
27
+ .option("-b, --brain <name>", "Brain (team memory) name on the server")
28
+ .option("-t, --token <token>", "Auth token (stored in ~/.aidimag/credentials.json, NOT the repo)")
29
+ .option("--json", "Machine-readable output (remote)")
30
+ .option("--id <memoryId>", "Show one remote memory by id (remote)")
31
+ .option("--limit <n>", "Max rows to list (remote)", "20")
32
+ .option("--summary", "Counts only — skip listing rows (remote)")
33
+ .option("--proposals", "List proposals instead of memories (remote)")
34
+ .option("--all", "With --proposals: include resolved rows (default: pending only)")
35
+ .option("--full", "Include full remote payload JSON (remote)")
36
+ .action(async (action, opts) => {
37
+ const root = findRepoRoot() ?? fail("not inside a repo");
38
+ const { readCloudConfig, writeCloudConfig, saveToken, getToken, fetchRemoteSnapshot, syncMetaKey } = await import("../../sync/client.js");
39
+ switch (action) {
40
+ case "link": {
41
+ if (!opts.server || !opts.brain)
42
+ fail("usage: dim cloud link --server <url> --brain <name> [--token <token>]");
43
+ const server = String(opts.server).replace(/\/$/, "");
44
+ writeCloudConfig(root, { server, brain: opts.brain });
45
+ if (opts.token)
46
+ saveToken(server, opts.token);
47
+ console.log(`Linked to ${server} (brain: ${opts.brain}).`);
48
+ console.log(`Config in .aidimag/config.json (commit it — no secrets inside). Token in ~/.aidimag/credentials.json.`);
49
+ if (!opts.token && !getToken(server)) {
50
+ console.log("⚠ No token stored yet — pass --token or set AIDIMAG_API_KEY before `dim sync`.");
51
+ }
52
+ break;
53
+ }
54
+ case "unlink": {
55
+ writeCloudConfig(root, { server: "", brain: "" });
56
+ console.log("Unlinked (config cleared).");
57
+ break;
58
+ }
59
+ case "status": {
60
+ const cfg = readCloudConfig(root);
61
+ if (!cfg)
62
+ console.log("Not cloud-linked. Use `dim cloud link`.");
63
+ else
64
+ console.log(`server: ${cfg.server}\nbrain: ${cfg.brain}\ntoken: ${getToken(cfg.server) ? "stored" : "MISSING"}`);
65
+ break;
66
+ }
67
+ case "remote": {
68
+ const cfg = readCloudConfig(root);
69
+ if (!cfg)
70
+ fail("not cloud-linked. Use `dim cloud link` first.");
71
+ const store = MemoryStore.open(root);
72
+ let localCursor = 0;
73
+ let localMemories = 0;
74
+ try {
75
+ localCursor = parseInt(store.getMeta(syncMetaKey("sync_pull_cursor", cfg.brain)) ?? "0", 10);
76
+ localMemories = store.statusSummary().total;
77
+ }
78
+ finally {
79
+ store.close();
80
+ }
81
+ const snapshot = await fetchRemoteSnapshot(root, {
82
+ id: opts.id,
83
+ tbl: opts.proposals ? "proposals" : undefined,
84
+ limit: parseInt(String(opts.limit ?? "20"), 10),
85
+ list: opts.summary ? false : opts.id ? false : true,
86
+ full: Boolean(opts.full || opts.id),
87
+ all: Boolean(opts.all),
88
+ });
89
+ if (opts.json) {
90
+ console.log(JSON.stringify({ server: cfg.server, localCursor, localMemories, ...snapshot }, null, 2));
91
+ break;
92
+ }
93
+ if (snapshot.item) {
94
+ const m = snapshot.item;
95
+ console.log(`☁ Remote ${m.tbl.slice(0, -1)} — ${m.id}`);
96
+ console.log(`server: ${cfg.server}`);
97
+ console.log(`brain: ${snapshot.brain}`);
98
+ if (m.kind)
99
+ console.log(`kind: ${m.kind}`);
100
+ if (m.status)
101
+ console.log(`status: ${m.status}`);
102
+ console.log(`updated: ${m.updatedAt}`);
103
+ if (m.claim)
104
+ console.log(`\n${m.claim}`);
105
+ if (m.payload && opts.full)
106
+ console.log(`\n${JSON.stringify(m.payload, null, 2)}`);
107
+ break;
108
+ }
109
+ const pending = Math.max(0, snapshot.seq - localCursor);
110
+ console.log(`☁ Remote snapshot — ${snapshot.brain}`);
111
+ console.log(`server: ${cfg.server}`);
112
+ console.log(`seq: ${snapshot.seq}${pending ? ` (local cursor ${localCursor} — ${pending} update${pending === 1 ? "" : "s"} behind)` : " (up to date with local cursor)"}`);
113
+ console.log(`memories: ${snapshot.counts.memories} on server Ā· ${localMemories} local`);
114
+ console.log(`proposals: ${snapshot.counts.proposals} pending on server`);
115
+ if (snapshot.counts.tombstones)
116
+ console.log(`tombstones: ${snapshot.counts.tombstones} on server`);
117
+ if (localMemories > 0 && snapshot.counts.memories === 0) {
118
+ console.log("\n⚠ Local memories exist but remote is empty — run `dim sync` to confirm upload.");
119
+ }
120
+ else if (pending) {
121
+ console.log(`\nRun \`dim sync\` to pull remote changes.`);
122
+ }
123
+ if (snapshot.items?.length) {
124
+ const heading = opts.proposals ? "Remote proposals" : "Remote memories";
125
+ console.log(`\n${heading}:`);
126
+ for (const row of snapshot.items) {
127
+ const claim = row.claim ? row.claim.replace(/\s+/g, " ").slice(0, 72) : "";
128
+ const suffix = row.claim && row.claim.length > 72 ? "…" : "";
129
+ console.log(` ${(row.status ?? "—").padEnd(11)} ${(row.kind ?? "—").padEnd(10)} ${row.id.slice(0, 12).padEnd(12)} ${claim}${suffix}`);
130
+ }
131
+ }
132
+ else if (!opts.summary) {
133
+ console.log("\n(no remote rows to list)");
134
+ }
135
+ break;
136
+ }
137
+ default:
138
+ fail(`unknown action '${action}'. Use: link | unlink | status | remote`);
139
+ }
140
+ });
141
+ program
142
+ .command("login")
143
+ .description("Log this device in to the sync server (device-code flow, approved in the browser)")
144
+ .option("-s, --server <url>", "Server URL (defaults to the repo's linked server)")
145
+ .option("--no-open", "Don't open the browser automatically")
146
+ .action(async (opts) => {
147
+ const { readCloudConfig, startDeviceLogin, pollDeviceLogin } = await import("../../sync/client.js");
148
+ const root = findRepoRoot();
149
+ const server = opts.server?.replace(/\/$/, "") ?? (root ? readCloudConfig(root)?.server : undefined);
150
+ if (!server)
151
+ fail("no server: pass --server <url> or link the repo with `dim cloud link` first");
152
+ const start = await startDeviceLogin(server);
153
+ const approveUrl = `${start.verification_uri}?code=${encodeURIComponent(start.user_code)}`;
154
+ console.log(`\nTo approve this device, open:\n\n ${approveUrl}\n\nand confirm the code: ${start.user_code}\n`);
155
+ if (opts.open)
156
+ await openBrowser(approveUrl);
157
+ console.log("Waiting for approval…");
158
+ const { brain } = await pollDeviceLogin(server, start);
159
+ console.log(`āœ“ Logged in to ${server} (scope: ${brain ?? "all brains"}). Token saved to ~/.aidimag/credentials.json.`);
160
+ });
161
+ program
162
+ .command("logout")
163
+ .description("Remove this device's stored token for the sync server")
164
+ .option("-s, --server <url>", "Server URL (defaults to the repo's linked server)")
165
+ .action(async (opts) => {
166
+ const { readCloudConfig, removeToken } = await import("../../sync/client.js");
167
+ const root = findRepoRoot();
168
+ const server = opts.server?.replace(/\/$/, "") ?? (root ? readCloudConfig(root)?.server : undefined);
169
+ if (!server)
170
+ fail("no server: pass --server <url> or link the repo with `dim cloud link` first");
171
+ console.log(removeToken(server) ? `āœ“ Logged out of ${server}.` : `No stored token for ${server}.`);
172
+ });
173
+ program
174
+ .command("sync")
175
+ .description("Sync this repo's memory with the linked team server (push + pull)")
176
+ .option("--full", "Upload all local memories to this brain (not just changes since last push)")
177
+ .option("-y, --yes", "Skip confirmation when uploading to an empty remote brain")
178
+ .action(async (opts) => {
179
+ const root = findRepoRoot() ?? fail("not inside a repo");
180
+ const store = MemoryStore.open(root);
181
+ const { sync } = await import("../../sync/client.js");
182
+ try {
183
+ const r = await sync(store, root, {
184
+ full: Boolean(opts.full),
185
+ confirmFullUpload: opts.full
186
+ ? undefined
187
+ : async (localCount, remoteCount) => {
188
+ if (opts.yes)
189
+ return true;
190
+ if (!process.stdin.isTTY) {
191
+ const remoteHint = remoteCount === null
192
+ ? "could not read remote memory count"
193
+ : remoteCount === 0
194
+ ? "remote brain is empty"
195
+ : `remote has ${remoteCount} ${remoteCount === 1 ? "memory" : "memories"}, local has ${localCount}`;
196
+ console.log(`${remoteHint}. Run \`dim sync --full\` (or \`dim sync -y\`) to upload all local memories.`);
197
+ return false;
198
+ }
199
+ const prompter = await createPrompter();
200
+ try {
201
+ const prompt = remoteCount === 0 || remoteCount === null
202
+ ? `Remote brain is empty but you have ${localCount} local ${localCount === 1 ? "memory" : "memories"}. Upload all to the cloud? [y/N] `
203
+ : `You have ${localCount} local ${localCount === 1 ? "memory" : "memories"} but only ${remoteCount} on the remote. Upload all local memories? [y/N] `;
204
+ const answer = await prompter.ask(prompt);
205
+ return /^y(es)?$/i.test(answer.trim());
206
+ }
207
+ finally {
208
+ prompter.close();
209
+ }
210
+ },
211
+ });
212
+ const recv = r.applied
213
+ ? `received ${r.applied} update${r.applied === 1 ? "" : "s"} from the team`
214
+ : "nothing new from the team";
215
+ const mem = (n) => `${n} ${n === 1 ? "memory" : "memories"}`;
216
+ const sent = r.memoriesPushed
217
+ ? `sent ${mem(r.memoriesPushed)}`
218
+ : r.memoriesQueued
219
+ ? `already on server (${mem(r.memoriesQueued)} unchanged)`
220
+ : "nothing to send";
221
+ console.log(`☁ Synced — ${sent}, ${recv}${r.eventsPushed ? ` (+${r.eventsPushed} verification events)` : ""}.`);
222
+ const localMemories = store.statusSummary().total;
223
+ if (opts.full && r.memoriesQueued > 0 && r.memoriesPushed === 0 && localMemories > 0) {
224
+ console.log("āœ“ Full sync checked all local memories — cloud already has them (no updates needed).");
225
+ }
226
+ else if (opts.full && localMemories === 0 && r.pushQueued === 0) {
227
+ console.log("⚠ memory.db has 0 memories — nothing to upload with --full.");
228
+ console.log(" If you expected memories here, they may have been deleted locally by a prior sync pull (tombstones).");
229
+ console.log(" Check `dim status` and restore from backup if needed.");
230
+ }
231
+ else if (r.needsFullUploadConfirm) {
232
+ console.log("Skipped uploading local memories — run `dim sync` again and confirm, or use `dim sync --full`.");
233
+ }
234
+ else if (r.pushSkipped) {
235
+ console.log("⚠ Local memories were not uploaded (incremental sync only sends changes since the last push to this brain).");
236
+ console.log(" Run `dim sync --full` to upload all local memories to the cloud.");
237
+ }
238
+ // a pull that changed local memory should refresh the generated context
239
+ if (r.applied)
240
+ await maybeRegenerateContext(store);
241
+ }
242
+ catch (err) {
243
+ fail(err instanceof Error ? err.message : String(err));
244
+ }
245
+ finally {
246
+ store.close();
247
+ }
248
+ });
249
+ program
250
+ .command("keys")
251
+ .description("Manage brain-scoped API keys on the sync server (admin token required)")
252
+ .argument("<action>", "create | list | revoke")
253
+ .option("-s, --server <url>", "Server URL (defaults to the repo's linked server)")
254
+ .option("-b, --brain <name>", "Brain the key grants access to (create)")
255
+ .option("-l, --label <text>", "Key label, e.g. 'ci' or 'alice-laptop' (create)")
256
+ .option("-k, --key <key>", "Key to revoke")
257
+ .option("-t, --admin-token <token>", "Admin token (or AIDIMAG_ADMIN_TOKEN env)")
258
+ .action(async (action, opts) => {
259
+ const { readCloudConfig } = await import("../../sync/client.js");
260
+ const root = findRepoRoot();
261
+ const server = opts.server ?? (root ? readCloudConfig(root)?.server : undefined);
262
+ if (!server)
263
+ fail("no server: pass --server or link the repo with `dim cloud link`");
264
+ const admin = opts.adminToken ?? process.env.AIDIMAG_ADMIN_TOKEN;
265
+ if (!admin)
266
+ fail("provide --admin-token or set AIDIMAG_ADMIN_TOKEN");
267
+ const call = async (method, pathq, body) => {
268
+ const res = await fetch(`${server}${pathq}`, {
269
+ method,
270
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${admin}` },
271
+ body: body ? JSON.stringify(body) : undefined,
272
+ });
273
+ const json = await res.json();
274
+ if (!res.ok)
275
+ fail(`server: ${JSON.stringify(json)}`);
276
+ return json;
277
+ };
278
+ switch (action) {
279
+ case "create": {
280
+ if (!opts.brain)
281
+ fail("usage: dim keys create --brain <name> [--label <text>]");
282
+ const r = (await call("POST", "/v1/keys", { brain: opts.brain, label: opts.label }));
283
+ console.log(`Created key for brain '${opts.brain}':\n${r.key}\n\n⚠ Shown once — store it now (teammates: dim cloud link --token <key>).`);
284
+ break;
285
+ }
286
+ case "list": {
287
+ const r = (await call("GET", "/v1/keys"));
288
+ if (!r.keys.length)
289
+ console.log("No keys.");
290
+ for (const k of r.keys) {
291
+ console.log(`${k.revoked_at ? "āœ—" : "āœ“"} ${k.key} brain=${k.brain}${k.label ? ` label=${k.label}` : ""}${k.revoked_at ? " (revoked)" : ""}`);
292
+ }
293
+ break;
294
+ }
295
+ case "revoke": {
296
+ if (!opts.key)
297
+ fail("usage: dim keys revoke --key <full-key>");
298
+ const r = (await call("DELETE", `/v1/keys?key=${encodeURIComponent(opts.key)}`));
299
+ console.log(r.revoked ? "Key revoked." : "Key not found (or already revoked).");
300
+ break;
301
+ }
302
+ default:
303
+ fail(`unknown action '${action}'. Use: create | list | revoke`);
304
+ }
305
+ });
306
+ }
307
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Ticketing commands: ticket (connect/status/disconnect/show/share/branch-rule),
3
+ * branch, branch-check (hidden hook helper).
4
+ */
5
+ import type { Command } from "commander";
6
+ export declare function registerTicketCommands(program: Command): void;
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Ticketing commands: ticket (connect/status/disconnect/show/share/branch-rule),
3
+ * branch, branch-check (hidden hook helper).
4
+ */
5
+ import { findRepoRoot } from "../../db/store.js";
6
+ import { fail, createPrompter, openBrowser } from "../shared.js";
7
+ const TICKET_PROVIDERS = ["jira", "github", "linear", "http", "remote"];
8
+ export function registerTicketCommands(program) {
9
+ program
10
+ .command("ticket")
11
+ .description("Connect a ticketing app so proposals carry real context (Jira, GitHub Issues, Linear, the team sync server, or your own HTTP middleware)")
12
+ .argument("<action>", "connect | status | disconnect | show | share | branch-rule")
13
+ .argument("[id]", "Ticket id for 'show' (e.g. XXX-2100 or #123) Ā· provider name for 'connect' (jira|github|linear|http|remote)")
14
+ .option("--provider <name>", "jira | github | linear | http | remote (connect/share)")
15
+ .option("--url <baseUrl>", "Jira site / GitHub repo URL / middleware endpoint (connect/share)")
16
+ .option("--token <credential>", "Jira: email:apiToken or PAT Ā· GitHub/Linear: token Ā· http: optional bearer (connect/share)")
17
+ .option("--pattern <regex>", "Ticket-id pattern for branch/commit extraction (connect) Ā· branch pattern (branch-rule)")
18
+ .option("--enforce <mode>", "push | warn | off (branch-rule)")
19
+ .option("--exempt <branches...>", "Exempt branch regexes, e.g. main develop 'release/.*' (branch-rule)")
20
+ .option("--print <host>", "Emit the server-side rule for github | gitlab | bitbucket (branch-rule)")
21
+ .option("--remove", "Remove the team ticket config from the sync server (share)")
22
+ .option("--admin-token <token>", "Sync-server admin token (share; or AIDIMAG_ADMIN_TOKEN env)")
23
+ .option("--no-open", "Don't open the API-token page in the browser (connect)")
24
+ .action(async (action, id, opts) => {
25
+ const root = findRepoRoot() ?? fail("not inside a repo");
26
+ const tickets = await import("../../tickets/provider.js");
27
+ switch (action) {
28
+ case "connect": {
29
+ // provider: positional (`dim ticket connect jira`), flag, or asked interactively
30
+ let provider = (id ?? opts.provider)?.toLowerCase();
31
+ if (provider && !TICKET_PROVIDERS.includes(provider)) {
32
+ fail(`unknown provider '${provider}' — use ${TICKET_PROVIDERS.join(", ")}`);
33
+ }
34
+ // Prompt for anything missing — the prompter queues piped lines, so
35
+ // scripted/agent-driven input works too; on closed stdin answers come
36
+ // back empty and we fail fast instead of hanging (CI-safe).
37
+ const needsUrl = !opts.url && provider !== "linear" && provider !== "remote";
38
+ const interactive = !provider || (provider !== "remote" && (needsUrl || !opts.token));
39
+ const isTTY = Boolean(process.stdin.isTTY && process.stdout.isTTY);
40
+ const existing = tickets.readTicketsConfig(root);
41
+ const { ask, close } = interactive ? await createPrompter() : { ask: async () => "", close: () => undefined };
42
+ try {
43
+ if (!provider) {
44
+ console.log("šŸŽ« Let's connect your ticketing app — proposals will carry the *why* from your tickets.\n");
45
+ const ans = (await ask(` Which one? [${TICKET_PROVIDERS.join(" | ")}] `)).trim().toLowerCase();
46
+ if (!TICKET_PROVIDERS.includes(ans))
47
+ fail(`unknown provider '${ans}' — use ${TICKET_PROVIDERS.join(", ")}`);
48
+ provider = ans;
49
+ }
50
+ // ---- remote: zero local credentials — the sync server is the middleman
51
+ if (provider === "remote") {
52
+ const { readCloudConfig, getToken } = await import("../../sync/client.js");
53
+ const cloud = readCloudConfig(root);
54
+ if (!cloud)
55
+ fail("remote tickets ride the sync channel — link the repo first: dim cloud link (then an admin runs `dim ticket share`)");
56
+ tickets.writeTicketsConfig(root, {
57
+ ...existing,
58
+ provider: "remote",
59
+ baseUrl: undefined,
60
+ pattern: opts.pattern ?? existing.pattern ?? tickets.DEFAULT_TICKET_PATTERN,
61
+ });
62
+ console.log(`šŸŽ« Connected via the team sync server (${cloud.server}, brain: ${cloud.brain}).`);
63
+ console.log(` Zero local ticket credentials — the server holds the team token.`);
64
+ if (!getToken(cloud.server))
65
+ console.log(` ⚠ No sync token on this machine yet — run \`dim login\` first.`);
66
+ // trust-building: check the server actually has a team config
67
+ try {
68
+ const res = await fetch(`${cloud.server}/v1/ticket-config?brain=${encodeURIComponent(cloud.brain)}`, {
69
+ headers: { Authorization: `Bearer ${getToken(cloud.server) ?? ""}` },
70
+ });
71
+ const body = (await res.json());
72
+ if (res.ok && body.config?.provider) {
73
+ console.log(` āœ“ Server is set up for ${body.config.provider} tickets — try \`dim ticket show <id>\`.`);
74
+ }
75
+ else {
76
+ console.log(` ⚠ The server has no team ticket config yet — an admin should run \`dim ticket share\`.`);
77
+ }
78
+ }
79
+ catch {
80
+ console.log(` (couldn't reach the server to check its ticket config — it may be offline)`);
81
+ }
82
+ break;
83
+ }
84
+ // ---- direct providers: jira | github | linear | http
85
+ let baseUrl = opts.url?.replace(/\/$/, "");
86
+ if (!baseUrl && provider !== "linear") {
87
+ const what = provider === "jira" ? "your Jira site URL (e.g. https://acme.atlassian.net)"
88
+ : provider === "github" ? "the repo URL (e.g. https://github.com/acme/api)"
89
+ : "your middleware endpoint (implements GET /ticket/:id)";
90
+ baseUrl = (await ask(` What's ${what}?\n › `)).trim().replace(/\/$/, "");
91
+ if (!baseUrl)
92
+ fail("a base URL is required");
93
+ }
94
+ let token = opts.token;
95
+ if (!token && interactive) {
96
+ const page = tickets.TOKEN_PAGES[provider];
97
+ if (page) {
98
+ console.log(`\n You'll need an API token — grab one here:\n ${page}`);
99
+ if (opts.open && isTTY)
100
+ await openBrowser(page);
101
+ }
102
+ const hint = provider === "jira" ? "email:apiToken (or a PAT)"
103
+ : provider === "http" ? "bearer token (enter to skip — optional for internal services)"
104
+ : "token";
105
+ token = (await ask(`\n Paste your ${hint}: `)).trim() || undefined;
106
+ }
107
+ if (!token && provider !== "http") {
108
+ console.log(` ⚠ No credential provided — you can add one later (re-run connect, or set AIDIMAG_TICKET_TOKEN).`);
109
+ }
110
+ const credKey = baseUrl ?? "linear";
111
+ tickets.writeTicketsConfig(root, {
112
+ ...existing,
113
+ provider,
114
+ baseUrl,
115
+ pattern: opts.pattern ?? existing.pattern ?? (provider === "github" ? "#\\d+" : tickets.DEFAULT_TICKET_PATTERN),
116
+ });
117
+ if (token)
118
+ tickets.saveTicketCredential(credKey, token);
119
+ console.log(`\nšŸŽ« Connected ${provider}${baseUrl ? ` at ${baseUrl}` : ""}.`);
120
+ console.log(` Config in .aidimag/config.json (commit it — no secrets inside).`);
121
+ if (token)
122
+ console.log(` Credential stored in ~/.aidimag/credentials.json (this machine only).`);
123
+ // trust-building: validate with a live round-trip
124
+ const p = tickets.ticketProviderFor(root);
125
+ if (p && interactive) {
126
+ const sample = (await ask(` Validate with a real ticket? Enter an id (or press enter to skip): `)).trim();
127
+ if (sample) {
128
+ const t = await p.getTicket(sample).catch((e) => fail(`validation fetch failed: ${e.message}`));
129
+ console.log(t ? ` āœ“ Validated — fetched ${t.id}: ā€œ${t.title}ā€` : ` ⚠ ${sample} not found (connection works, ticket doesn't exist)`);
130
+ }
131
+ else {
132
+ console.log(` Tip: validate any time with \`dim ticket show <id>\`.`);
133
+ }
134
+ }
135
+ else if (p) {
136
+ console.log(` Tip: validate with \`dim ticket show <id>\`.`);
137
+ }
138
+ }
139
+ finally {
140
+ close();
141
+ }
142
+ break;
143
+ }
144
+ case "status": {
145
+ const cfg = tickets.readTicketsConfig(root);
146
+ if (!cfg.provider) {
147
+ console.log("No ticketing app connected. Run `dim ticket connect` to set one up interactively.");
148
+ break;
149
+ }
150
+ if (cfg.provider === "remote") {
151
+ const { readCloudConfig, getToken } = await import("../../sync/client.js");
152
+ const cloud = readCloudConfig(root);
153
+ console.log(`provider: remote (via the team sync server)\nserver: ${cloud?.server ?? "NOT LINKED — dim cloud link"}\nbrain: ${cloud?.brain ?? "—"}\npattern: ${cfg.pattern ?? tickets.DEFAULT_TICKET_PATTERN}\ntoken: ${cloud && getToken(cloud.server) ? "sync token stored" : "MISSING — dim login"}`);
154
+ }
155
+ else {
156
+ const credKey = cfg.baseUrl ?? "linear";
157
+ console.log(`provider: ${cfg.provider}${cfg.baseUrl ? `\nbaseUrl: ${cfg.baseUrl}` : ""}\npattern: ${cfg.pattern ?? tickets.DEFAULT_TICKET_PATTERN}\ntoken: ${tickets.getTicketCredential(credKey) ? "stored" : "MISSING"}`);
158
+ }
159
+ const branch = cfg.branch;
160
+ if (branch?.pattern)
161
+ console.log(`branch: ${branch.pattern} (enforce: ${branch.enforce ?? "off"})`);
162
+ break;
163
+ }
164
+ case "disconnect": {
165
+ const existing = tickets.readTicketsConfig(root);
166
+ tickets.writeTicketsConfig(root, { branch: existing.branch }); // keep branch rules, drop provider
167
+ console.log("šŸŽ« Disconnected (credential kept in ~/.aidimag/credentials.json — remove manually if needed).");
168
+ break;
169
+ }
170
+ case "show": {
171
+ if (!id)
172
+ fail("usage: dim ticket show <id>");
173
+ const p = tickets.ticketProviderFor(root) ?? fail("no ticketing app connected (or credential missing) — run `dim ticket connect` first");
174
+ const t = await p.getTicket(id);
175
+ if (!t)
176
+ fail(`ticket ${id} not found`);
177
+ console.log(`šŸŽ« ${t.id} — ${t.title}\n ${t.type} Ā· ${t.status}${t.labels.length ? ` Ā· ${t.labels.join(", ")}` : ""}${t.parent ? `\n part of ${t.parent.id} ā€œ${t.parent.title}ā€` : ""}\n ${t.url}`);
178
+ if (t.body)
179
+ console.log(`\n${t.body}`);
180
+ break;
181
+ }
182
+ // T3: admin pushes the team's ticket credential to the sync server —
183
+ // teammates then run `dim ticket connect remote` and never hold a token.
184
+ case "share": {
185
+ const { readCloudConfig } = await import("../../sync/client.js");
186
+ const cloud = readCloudConfig(root) ?? fail("repo is not cloud-linked — run `dim cloud link` first");
187
+ const admin = opts.adminToken ?? process.env.AIDIMAG_ADMIN_TOKEN;
188
+ if (!admin)
189
+ fail("provide --admin-token or set AIDIMAG_ADMIN_TOKEN (share configures TEAM credentials — admin only)");
190
+ const endpoint = `${cloud.server}/v1/ticket-config?brain=${encodeURIComponent(cloud.brain)}`;
191
+ if (opts.remove) {
192
+ const res = await fetch(endpoint, { method: "DELETE", headers: { Authorization: `Bearer ${admin}` } });
193
+ const body = (await res.json());
194
+ if (!res.ok)
195
+ fail(`server: ${body.error ?? res.status}`);
196
+ console.log(body.removed ? "šŸŽ« Team ticket config removed from the server." : "No team ticket config to remove.");
197
+ break;
198
+ }
199
+ const local = tickets.readTicketsConfig(root);
200
+ const provider = (opts.provider ?? (local.provider !== "remote" ? local.provider : undefined));
201
+ if (!provider)
202
+ fail("usage: dim ticket share --provider jira|github|linear|http --url <baseUrl> --token <credential> (defaults come from this repo's `dim ticket connect`)");
203
+ const baseUrl = opts.url?.replace(/\/$/, "") ?? local.baseUrl ?? "";
204
+ const credential = opts.token ?? tickets.getTicketCredential(baseUrl || "linear") ?? undefined;
205
+ if (!credential && provider !== "http")
206
+ fail("no credential to share — pass --token (or connect locally first so it can be reused)");
207
+ const res = await fetch(endpoint, {
208
+ method: "PUT",
209
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${admin}` },
210
+ body: JSON.stringify({ provider, baseUrl, credential }),
211
+ });
212
+ const body = (await res.json());
213
+ if (!res.ok)
214
+ fail(`server: ${body.error ?? res.status}`);
215
+ console.log(`šŸŽ« Team ticket config stored on ${cloud.server} (brain: ${cloud.brain}, provider: ${provider}).`);
216
+ console.log(` Teammates: \`dim ticket connect remote\` — zero local ticket credentials, server-side caching.`);
217
+ break;
218
+ }
219
+ // T1.5/T3: manage the branch convention + emit the matching server-side rule
220
+ case "branch-rule": {
221
+ const existing = tickets.readTicketsConfig(root);
222
+ if (opts.pattern || opts.enforce || opts.exempt) {
223
+ const enforce = opts.enforce;
224
+ if (enforce && !["push", "warn", "off"].includes(enforce))
225
+ fail(`--enforce must be push, warn, or off (got '${enforce}')`);
226
+ tickets.writeTicketsConfig(root, {
227
+ ...existing,
228
+ branch: {
229
+ ...existing.branch,
230
+ ...(opts.pattern ? { pattern: opts.pattern } : {}),
231
+ ...(enforce ? { enforce } : {}),
232
+ ...(opts.exempt ? { exempt: opts.exempt } : {}),
233
+ },
234
+ });
235
+ console.log("🌿 Branch convention saved to .aidimag/config.json (commit it — every member's hooks enforce it after `dim init`).");
236
+ }
237
+ const rules = tickets.readTicketsConfig(root).branch ?? {};
238
+ if (!rules.pattern) {
239
+ console.log("No branch convention configured. Set one with:\n dim ticket branch-rule --pattern '^(feature|bugfix|hotfix|chore)/[A-Z][A-Z0-9]+-\\d+(-[a-z0-9-]+)?$' --enforce push");
240
+ break;
241
+ }
242
+ if (!opts.print) {
243
+ console.log(`pattern: ${rules.pattern}\nenforce: ${rules.enforce ?? "off"}\nexempt: ${(rules.exempt ?? ["main", "master", "develop", "release/.*", "HEAD"]).join(", ")}`);
244
+ console.log(`\nCatch --no-verify bypassers with a server-side rule: dim ticket branch-rule --print github|gitlab|bitbucket`);
245
+ break;
246
+ }
247
+ const host = String(opts.print).toLowerCase();
248
+ const exempt = rules.exempt ?? ["main", "master", "develop", "release/.*"];
249
+ if (host === "github") {
250
+ console.log(`GitHub ruleset (Settings → Rules → Rulesets → New branch ruleset → import JSON),\nor: gh api repos/{owner}/{repo}/rulesets --input ruleset.json\n`);
251
+ console.log(JSON.stringify({
252
+ name: "aidimag branch convention",
253
+ target: "branch",
254
+ enforcement: rules.enforce === "push" ? "active" : "evaluate",
255
+ conditions: { ref_name: { include: ["~ALL"], exclude: exempt.map((e) => `refs/heads/${e}`) } },
256
+ rules: [{ type: "branch_name_pattern", parameters: { operator: "regex", pattern: rules.pattern, negate: false, name: "ticket-prefixed branches" } }],
257
+ }, null, 2));
258
+ }
259
+ else if (host === "gitlab") {
260
+ console.log(`GitLab push rules (Settings → Repository → Push rules → Branch name), or via API:\n`);
261
+ console.log(` curl --request PUT --header "PRIVATE-TOKEN: <token>" \\\n "https://gitlab.example.com/api/v4/projects/<id>/push_rule" \\\n --data-urlencode "branch_name_regex=${rules.pattern}"`);
262
+ console.log(`\nNote: exempt branches (${exempt.join(", ")}) should be protected branches — push rules don't apply to them.`);
263
+ }
264
+ else if (host === "bitbucket") {
265
+ console.log(`Bitbucket branch restrictions (Repository settings → Branch restrictions), or via API:\n`);
266
+ console.log(JSON.stringify({ kind: "branch-name-pattern", pattern: rules.pattern, note: "Requires Premium; exempt: " + exempt.join(", ") }, null, 2));
267
+ }
268
+ else {
269
+ fail(`unknown host '${opts.print}' — use github, gitlab, or bitbucket`);
270
+ }
271
+ break;
272
+ }
273
+ default:
274
+ fail(`unknown action '${action}'. Use: connect | status | disconnect | show | share | branch-rule`);
275
+ }
276
+ });
277
+ program
278
+ .command("branch")
279
+ .description("Create a convention-conforming branch for a ticket (fetches the title for the slug when connected)")
280
+ .argument("<ticketId>", "e.g. XXX-2100")
281
+ .option("-p, --prefix <prefix>", "Branch prefix", "feature")
282
+ .action(async (ticketId, opts) => {
283
+ const root = findRepoRoot() ?? fail("not inside a git repo");
284
+ const { ticketProviderFor, buildBranchName } = await import("../../tickets/provider.js");
285
+ const provider = ticketProviderFor(root);
286
+ let title;
287
+ if (provider) {
288
+ const t = await provider.getTicket(ticketId).catch(() => null);
289
+ title = t?.title;
290
+ if (t)
291
+ console.log(`šŸŽ« ${t.id}: ā€œ${t.title}ā€`);
292
+ }
293
+ const name = buildBranchName(ticketId, title, opts.prefix);
294
+ const { execFileSync } = await import("node:child_process");
295
+ execFileSync("git", ["checkout", "-b", name], { cwd: root, stdio: "inherit" });
296
+ console.log(`🌿 You're on ${name} — commits here will carry ${ticketId} automatically.`);
297
+ });
298
+ program
299
+ .command("branch-check", { hidden: true })
300
+ .description("Validate the current branch against the team convention (used by git hooks)")
301
+ .option("--warn", "Warn only (post-checkout)")
302
+ .option("--push", "Exit 1 on violation when enforce mode is 'push' (pre-push)")
303
+ .action(async (opts) => {
304
+ const root = findRepoRoot();
305
+ if (!root)
306
+ return;
307
+ const { checkBranchName } = await import("../../tickets/provider.js");
308
+ const { execFileSync } = await import("node:child_process");
309
+ let branch = "";
310
+ try {
311
+ branch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: root, encoding: "utf8" }).trim();
312
+ }
313
+ catch {
314
+ return; // detached HEAD / not a repo — nothing to check
315
+ }
316
+ const r = checkBranchName(root, branch);
317
+ if (r.ok || r.exempt || r.enforce === "off")
318
+ return;
319
+ const fixHint = `git branch -m ${branch} <conforming-name> (or next time: dim branch <TICKET-ID>)`;
320
+ if (opts.push && r.enforce === "push") {
321
+ console.error(`\n🌿 aidimag: branch '${branch}' doesn't match the team convention (${r.pattern}).`);
322
+ console.error(` Pushes of non-conforming branches are blocked. Rename with:\n ${fixHint}\n`);
323
+ process.exit(1);
324
+ }
325
+ console.error(`🌿 aidimag: heads up — '${branch}' doesn't match the team's branch convention (${r.pattern}). Fix: ${fixHint}`);
326
+ });
327
+ }
328
+ //# sourceMappingURL=tickets.js.map
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Verification & guardrail commands: verify, check, brief.
3
+ */
4
+ import type { Command } from "commander";
5
+ export declare function registerVerifyCommands(program: Command): void;