aidimag 1.0.11 → 1.0.13
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/dist/cli/commands/sync.js +47 -12
- package/dist/cli/commands/tickets.js +3 -3
- package/dist/sync/client.d.ts +2 -4
- package/dist/sync/client.js +9 -32
- package/dist/tickets/provider.js +1 -3
- package/dist/ui/server.js +20 -9
- package/package.json +1 -1
|
@@ -37,7 +37,7 @@ export function registerSyncCommands(program) {
|
|
|
37
37
|
.option("--full", "Include full remote payload JSON (remote)")
|
|
38
38
|
.action(async (action, opts) => {
|
|
39
39
|
const root = findRepoRoot() ?? fail("not inside a repo");
|
|
40
|
-
const { readCloudConfig, writeCloudConfig,
|
|
40
|
+
const { readCloudConfig, writeCloudConfig, getToken, fetchRemoteSnapshot, syncMetaKey } = await import("../../sync/client.js");
|
|
41
41
|
switch (action) {
|
|
42
42
|
case "link": {
|
|
43
43
|
if (!opts.server || !opts.brain)
|
|
@@ -165,29 +165,64 @@ export function registerSyncCommands(program) {
|
|
|
165
165
|
.action(async (opts) => {
|
|
166
166
|
const { readCloudConfig, startDeviceLogin, pollDeviceLogin } = await import("../../sync/client.js");
|
|
167
167
|
const root = findRepoRoot();
|
|
168
|
-
|
|
168
|
+
if (!root)
|
|
169
|
+
fail("not inside a repo — run this command from a project directory");
|
|
170
|
+
const cfg = readCloudConfig(root);
|
|
171
|
+
const server = opts.server?.replace(/\/$/, "") ?? cfg?.server;
|
|
169
172
|
if (!server)
|
|
170
173
|
fail("no server: pass --server <url> or link the repo with `dim cloud link` first");
|
|
171
|
-
|
|
174
|
+
// Pass the brain so the approval page knows which project is requesting access
|
|
175
|
+
const requestedBrain = cfg?.brain;
|
|
176
|
+
const start = await startDeviceLogin(server, requestedBrain);
|
|
172
177
|
const approveUrl = `${start.verification_uri}?code=${encodeURIComponent(start.user_code)}`;
|
|
173
178
|
console.log(`\nTo approve this device, open:\n\n ${approveUrl}\n\nand confirm the code: ${start.user_code}\n`);
|
|
174
179
|
if (opts.open)
|
|
175
180
|
await openBrowser(approveUrl);
|
|
176
181
|
console.log("Waiting for approval…");
|
|
177
|
-
const { brain } = await pollDeviceLogin(server, start);
|
|
178
|
-
|
|
182
|
+
const { token, brain } = await pollDeviceLogin(server, start);
|
|
183
|
+
// Save token to project config
|
|
184
|
+
const p = configPath(root);
|
|
185
|
+
let existing = {};
|
|
186
|
+
try {
|
|
187
|
+
existing = JSON.parse(readFileSync(p, "utf8"));
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// fresh file
|
|
191
|
+
}
|
|
192
|
+
existing.token = token;
|
|
193
|
+
writeFileSync(p, JSON.stringify(existing, null, 2) + "\n");
|
|
194
|
+
console.log(`✓ Logged in to ${server} (scope: ${brain ?? "all brains"}).`);
|
|
195
|
+
console.log(`✓ Token saved to .aidimag/config.json (per-project).`);
|
|
196
|
+
console.log(`⚠ Add .aidimag/config.json to .gitignore if you don't want to commit the token.`);
|
|
179
197
|
});
|
|
180
198
|
program
|
|
181
199
|
.command("logout")
|
|
182
|
-
.description("Remove this device's stored token
|
|
183
|
-
.option("-s, --server <url>", "Server URL (defaults to the repo's linked server)")
|
|
200
|
+
.description("Remove this device's stored token from the project config")
|
|
184
201
|
.action(async (opts) => {
|
|
185
|
-
const { readCloudConfig
|
|
202
|
+
const { readCloudConfig } = await import("../../sync/client.js");
|
|
186
203
|
const root = findRepoRoot();
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
204
|
+
if (!root)
|
|
205
|
+
fail("not inside a repo — run this command from a project directory");
|
|
206
|
+
const cfg = readCloudConfig(root);
|
|
207
|
+
if (!cfg)
|
|
208
|
+
fail("not cloud-linked. Use `dim cloud link` first");
|
|
209
|
+
// Remove token from project config
|
|
210
|
+
const p = configPath(root);
|
|
211
|
+
let existing = {};
|
|
212
|
+
try {
|
|
213
|
+
existing = JSON.parse(readFileSync(p, "utf8"));
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
console.log("No config file found.");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (!existing.token) {
|
|
220
|
+
console.log("No token stored in this project.");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
delete existing.token;
|
|
224
|
+
writeFileSync(p, JSON.stringify(existing, null, 2) + "\n");
|
|
225
|
+
console.log(`✓ Logged out — token removed from .aidimag/config.json`);
|
|
191
226
|
});
|
|
192
227
|
program
|
|
193
228
|
.command("sync")
|
|
@@ -61,12 +61,12 @@ export function registerTicketCommands(program) {
|
|
|
61
61
|
});
|
|
62
62
|
console.log(`🎫 Connected via the team sync server (${cloud.server}, brain: ${cloud.brain}).`);
|
|
63
63
|
console.log(` Zero local ticket credentials — the server holds the team token.`);
|
|
64
|
-
if (!getToken(cloud.server))
|
|
64
|
+
if (!getToken(cloud.server, root))
|
|
65
65
|
console.log(` ⚠ No sync token on this machine yet — run \`dim login\` first.`);
|
|
66
66
|
// trust-building: check the server actually has a team config
|
|
67
67
|
try {
|
|
68
68
|
const res = await fetch(`${cloud.server}/v1/ticket-config?brain=${encodeURIComponent(cloud.brain)}`, {
|
|
69
|
-
headers: { Authorization: `Bearer ${getToken(cloud.server) ?? ""}` },
|
|
69
|
+
headers: { Authorization: `Bearer ${getToken(cloud.server, root) ?? ""}` },
|
|
70
70
|
});
|
|
71
71
|
const body = (await res.json());
|
|
72
72
|
if (res.ok && body.config?.provider) {
|
|
@@ -150,7 +150,7 @@ export function registerTicketCommands(program) {
|
|
|
150
150
|
if (cfg.provider === "remote") {
|
|
151
151
|
const { readCloudConfig, getToken } = await import("../../sync/client.js");
|
|
152
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"}`);
|
|
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, root) ? "sync token stored" : "MISSING — dim login"}`);
|
|
154
154
|
}
|
|
155
155
|
else {
|
|
156
156
|
const credKey = cfg.baseUrl ?? "linear";
|
package/dist/sync/client.d.ts
CHANGED
|
@@ -21,8 +21,6 @@ export declare function configPath(repoRoot: string): string;
|
|
|
21
21
|
export declare function readCloudConfig(repoRoot: string): CloudConfig | null;
|
|
22
22
|
export declare function writeCloudConfig(repoRoot: string, cfg: CloudConfig): void;
|
|
23
23
|
export declare function getToken(server: string, repoRoot?: string): string | null;
|
|
24
|
-
export declare function saveToken(server: string, token: string): void;
|
|
25
|
-
export declare function removeToken(server: string): boolean;
|
|
26
24
|
export interface DeviceStart {
|
|
27
25
|
device_code: string;
|
|
28
26
|
user_code: string;
|
|
@@ -31,8 +29,8 @@ export interface DeviceStart {
|
|
|
31
29
|
expires_in: number;
|
|
32
30
|
}
|
|
33
31
|
/** Begin a device-code login: server hands back a user code + approval URL. */
|
|
34
|
-
export declare function startDeviceLogin(server: string): Promise<DeviceStart>;
|
|
35
|
-
/** Poll until the device is approved in the browser.
|
|
32
|
+
export declare function startDeviceLogin(server: string, brain?: string): Promise<DeviceStart>;
|
|
33
|
+
/** Poll until the device is approved in the browser. Returns the token (caller must save it). */
|
|
36
34
|
export declare function pollDeviceLogin(server: string, start: DeviceStart): Promise<{
|
|
37
35
|
token: string;
|
|
38
36
|
brain: string | null;
|
package/dist/sync/client.js
CHANGED
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
* .aidimag/config.json { server, brain, token } — per-project, add to .gitignore if token is sensitive
|
|
10
10
|
* AIDIMAG_API_KEY env var — alternative to storing token in config
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync
|
|
13
|
-
import { homedir } from "node:os";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
14
13
|
import path from "node:path";
|
|
15
14
|
import { debugLog } from "../debug.js";
|
|
16
15
|
const CURSOR_KEY = "sync_pull_cursor";
|
|
@@ -52,9 +51,6 @@ export function writeCloudConfig(repoRoot, cfg) {
|
|
|
52
51
|
const brain = cfg.brain.trim();
|
|
53
52
|
writeFileSync(p, JSON.stringify({ ...existing, server, brain }, null, 2) + "\n");
|
|
54
53
|
}
|
|
55
|
-
function credentialsPath() {
|
|
56
|
-
return path.join(homedir(), ".aidimag", "credentials.json");
|
|
57
|
-
}
|
|
58
54
|
export function getToken(server, repoRoot) {
|
|
59
55
|
if (process.env.AIDIMAG_API_KEY)
|
|
60
56
|
return process.env.AIDIMAG_API_KEY;
|
|
@@ -74,31 +70,13 @@ export function getToken(server, repoRoot) {
|
|
|
74
70
|
}
|
|
75
71
|
return null;
|
|
76
72
|
}
|
|
77
|
-
export function saveToken(server, token) {
|
|
78
|
-
const p = credentialsPath();
|
|
79
|
-
mkdirSync(path.dirname(p), { recursive: true });
|
|
80
|
-
const creds = existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : {};
|
|
81
|
-
creds[server] = token;
|
|
82
|
-
writeFileSync(p, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
|
|
83
|
-
try {
|
|
84
|
-
chmodSync(p, 0o600);
|
|
85
|
-
}
|
|
86
|
-
catch { /* best-effort on Windows */ }
|
|
87
|
-
}
|
|
88
|
-
export function removeToken(server) {
|
|
89
|
-
const p = credentialsPath();
|
|
90
|
-
if (!existsSync(p))
|
|
91
|
-
return false;
|
|
92
|
-
const creds = JSON.parse(readFileSync(p, "utf8"));
|
|
93
|
-
if (!(server in creds))
|
|
94
|
-
return false;
|
|
95
|
-
delete creds[server];
|
|
96
|
-
writeFileSync(p, JSON.stringify(creds, null, 2) + "\n", { mode: 0o600 });
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
73
|
/** Begin a device-code login: server hands back a user code + approval URL. */
|
|
100
|
-
export async function startDeviceLogin(server) {
|
|
101
|
-
const res = await cloudFetch(server, `${server}/v1/auth/device`, {
|
|
74
|
+
export async function startDeviceLogin(server, brain) {
|
|
75
|
+
const res = await cloudFetch(server, `${server}/v1/auth/device`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: { "Content-Type": "application/json" },
|
|
78
|
+
body: brain ? JSON.stringify({ brain }) : undefined,
|
|
79
|
+
});
|
|
102
80
|
if (!res.ok) {
|
|
103
81
|
throw new Error(res.status === 404
|
|
104
82
|
? `server does not support device login (upgrade it to this aidimag version)`
|
|
@@ -106,7 +84,7 @@ export async function startDeviceLogin(server) {
|
|
|
106
84
|
}
|
|
107
85
|
return (await res.json());
|
|
108
86
|
}
|
|
109
|
-
/** Poll until the device is approved in the browser.
|
|
87
|
+
/** Poll until the device is approved in the browser. Returns the token (caller must save it). */
|
|
110
88
|
export async function pollDeviceLogin(server, start) {
|
|
111
89
|
const deadline = Date.now() + start.expires_in * 1000;
|
|
112
90
|
for (;;) {
|
|
@@ -123,7 +101,6 @@ export async function pollDeviceLogin(server, start) {
|
|
|
123
101
|
if (!res.ok)
|
|
124
102
|
throw new Error(`login failed: HTTP ${res.status} ${await res.text()}`);
|
|
125
103
|
const out = (await res.json());
|
|
126
|
-
saveToken(server, out.token);
|
|
127
104
|
return out;
|
|
128
105
|
}
|
|
129
106
|
}
|
|
@@ -396,7 +373,7 @@ export async function maybeAutoSync(store, repoRoot) {
|
|
|
396
373
|
const cfg = readCloudConfig(repoRoot);
|
|
397
374
|
if (!cfg)
|
|
398
375
|
return null;
|
|
399
|
-
if (!getToken(cfg.server))
|
|
376
|
+
if (!getToken(cfg.server, repoRoot))
|
|
400
377
|
return null;
|
|
401
378
|
const last = store.getMeta(AUTO_SYNC_LAST_KEY);
|
|
402
379
|
if (last && Date.now() - new Date(last).getTime() < AUTO_SYNC_DEBOUNCE_MS)
|
package/dist/tickets/provider.js
CHANGED
|
@@ -321,9 +321,7 @@ export function ticketProviderFor(repoRoot) {
|
|
|
321
321
|
const raw = JSON.parse(readFileSync(p, "utf8"));
|
|
322
322
|
if (!raw.server || !raw.brain)
|
|
323
323
|
return null;
|
|
324
|
-
const token = process.env.AIDIMAG_API_KEY ??
|
|
325
|
-
JSON.parse(readFileSync(credentialsPath(), "utf8"))[raw.server] ??
|
|
326
|
-
null;
|
|
324
|
+
const token = process.env.AIDIMAG_API_KEY ?? raw.token ?? null;
|
|
327
325
|
return token ? new RemoteProvider(raw.server, raw.brain, token) : null;
|
|
328
326
|
}
|
|
329
327
|
catch {
|
package/dist/ui/server.js
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
* same dashboard in a webview.
|
|
8
8
|
*/
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
|
-
import { watch, mkdirSync } from "node:fs";
|
|
10
|
+
import { watch, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
13
13
|
import { verifyAll } from "../verify/engine.js";
|
|
14
14
|
import { mineCommits } from "../capture/commit-miner.js";
|
|
15
15
|
import { hybridSearch, indexMemory, reindexAll } from "../embeddings/search.js";
|
|
16
|
-
import { readCloudConfig, writeCloudConfig,
|
|
16
|
+
import { readCloudConfig, writeCloudConfig, getToken, sync as cloudSync, configPath } from "../sync/client.js";
|
|
17
17
|
import { resolveKnowledgeConfig } from "../config.js";
|
|
18
18
|
import { ingestAll } from "../knowledge/ingest.js";
|
|
19
19
|
import { isAllowedSyncServerUrl } from "../security/url.js";
|
|
@@ -65,7 +65,7 @@ export function startUiServer(store, repoRoot, port = 4517) {
|
|
|
65
65
|
if (autoSyncMinutes > 0) {
|
|
66
66
|
const timer = setInterval(() => {
|
|
67
67
|
const cloud = readCloudConfig(repoRoot);
|
|
68
|
-
if (!cloud || !getToken(cloud.server))
|
|
68
|
+
if (!cloud || !getToken(cloud.server, repoRoot))
|
|
69
69
|
return;
|
|
70
70
|
cloudSync(store, repoRoot).catch(() => undefined);
|
|
71
71
|
}, autoSyncMinutes * 60 * 1000);
|
|
@@ -136,7 +136,7 @@ export function startUiServer(store, repoRoot, port = 4517) {
|
|
|
136
136
|
proposals: store.listProposals("PENDING", 200),
|
|
137
137
|
summary: store.statusSummary(),
|
|
138
138
|
cloud: cloud
|
|
139
|
-
? { server: cloud.server, brain: cloud.brain, hasToken: !!getToken(cloud.server) }
|
|
139
|
+
? { server: cloud.server, brain: cloud.brain, hasToken: !!getToken(cloud.server, repoRoot) }
|
|
140
140
|
: null,
|
|
141
141
|
tickets: tcfg.provider
|
|
142
142
|
? {
|
|
@@ -144,7 +144,7 @@ export function startUiServer(store, repoRoot, port = 4517) {
|
|
|
144
144
|
baseUrl: tcfg.baseUrl ?? null,
|
|
145
145
|
pattern: tcfg.pattern ?? DEFAULT_TICKET_PATTERN,
|
|
146
146
|
hasCredential: tcfg.provider === "remote"
|
|
147
|
-
? !!(cloud && getToken(cloud.server))
|
|
147
|
+
? !!(cloud && getToken(cloud.server, repoRoot))
|
|
148
148
|
: !!getTicketCredential(tcfg.baseUrl ?? "linear"),
|
|
149
149
|
branch: tcfg.branch ?? null,
|
|
150
150
|
}
|
|
@@ -232,10 +232,21 @@ export function startUiServer(store, repoRoot, port = 4517) {
|
|
|
232
232
|
json(res, 400, { error: "invalid server URL" });
|
|
233
233
|
return;
|
|
234
234
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
// Save to project config with token
|
|
236
|
+
const p = configPath(repoRoot);
|
|
237
|
+
let existing = {};
|
|
238
|
+
try {
|
|
239
|
+
existing = JSON.parse(readFileSync(p, "utf8"));
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// fresh file
|
|
243
|
+
}
|
|
244
|
+
const newConfig = { ...existing, server: serverUrl, brain: String(b.brain) };
|
|
245
|
+
if (b.token) {
|
|
246
|
+
newConfig.token = String(b.token);
|
|
247
|
+
}
|
|
248
|
+
writeFileSync(p, JSON.stringify(newConfig, null, 2) + "\n");
|
|
249
|
+
json(res, 200, { ok: true, hasToken: !!getToken(serverUrl, repoRoot) });
|
|
239
250
|
return;
|
|
240
251
|
}
|
|
241
252
|
if (req.method === "POST" && path === "/api/cloud/unlink") {
|