aidimag 1.0.11 → 1.0.12

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.
@@ -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, saveToken, getToken, fetchRemoteSnapshot, syncMetaKey } = await import("../../sync/client.js");
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,7 +165,9 @@ 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
- const server = opts.server?.replace(/\/$/, "") ?? (root ? readCloudConfig(root)?.server : undefined);
168
+ if (!root)
169
+ fail("not inside a repo — run this command from a project directory");
170
+ const server = opts.server?.replace(/\/$/, "") ?? readCloudConfig(root)?.server;
169
171
  if (!server)
170
172
  fail("no server: pass --server <url> or link the repo with `dim cloud link` first");
171
173
  const start = await startDeviceLogin(server);
@@ -174,20 +176,50 @@ export function registerSyncCommands(program) {
174
176
  if (opts.open)
175
177
  await openBrowser(approveUrl);
176
178
  console.log("Waiting for approval…");
177
- const { brain } = await pollDeviceLogin(server, start);
178
- console.log(`✓ Logged in to ${server} (scope: ${brain ?? "all brains"}). Token saved to ~/.aidimag/credentials.json.`);
179
+ const { token, brain } = await pollDeviceLogin(server, start);
180
+ // Save token to project config
181
+ const p = configPath(root);
182
+ let existing = {};
183
+ try {
184
+ existing = JSON.parse(readFileSync(p, "utf8"));
185
+ }
186
+ catch {
187
+ // fresh file
188
+ }
189
+ existing.token = token;
190
+ writeFileSync(p, JSON.stringify(existing, null, 2) + "\n");
191
+ console.log(`✓ Logged in to ${server} (scope: ${brain ?? "all brains"}).`);
192
+ console.log(`✓ Token saved to .aidimag/config.json (per-project).`);
193
+ console.log(`⚠ Add .aidimag/config.json to .gitignore if you don't want to commit the token.`);
179
194
  });
180
195
  program
181
196
  .command("logout")
182
- .description("Remove this device's stored token for the sync server")
183
- .option("-s, --server <url>", "Server URL (defaults to the repo's linked server)")
197
+ .description("Remove this device's stored token from the project config")
184
198
  .action(async (opts) => {
185
- const { readCloudConfig, removeToken } = await import("../../sync/client.js");
199
+ const { readCloudConfig } = await import("../../sync/client.js");
186
200
  const root = findRepoRoot();
187
- const server = opts.server?.replace(/\/$/, "") ?? (root ? readCloudConfig(root)?.server : undefined);
188
- if (!server)
189
- fail("no server: pass --server <url> or link the repo with `dim cloud link` first");
190
- console.log(removeToken(server) ? `✓ Logged out of ${server}.` : `No stored token for ${server}.`);
201
+ if (!root)
202
+ fail("not inside a repo — run this command from a project directory");
203
+ const cfg = readCloudConfig(root);
204
+ if (!cfg)
205
+ fail("not cloud-linked. Use `dim cloud link` first");
206
+ // Remove token from project config
207
+ const p = configPath(root);
208
+ let existing = {};
209
+ try {
210
+ existing = JSON.parse(readFileSync(p, "utf8"));
211
+ }
212
+ catch {
213
+ console.log("No config file found.");
214
+ return;
215
+ }
216
+ if (!existing.token) {
217
+ console.log("No token stored in this project.");
218
+ return;
219
+ }
220
+ delete existing.token;
221
+ writeFileSync(p, JSON.stringify(existing, null, 2) + "\n");
222
+ console.log(`✓ Logged out — token removed from .aidimag/config.json`);
191
223
  });
192
224
  program
193
225
  .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";
@@ -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;
@@ -32,7 +30,7 @@ export interface DeviceStart {
32
30
  }
33
31
  /** Begin a device-code login: server hands back a user code + approval URL. */
34
32
  export declare function startDeviceLogin(server: string): Promise<DeviceStart>;
35
- /** Poll until the device is approved in the browser. Saves the token on success. */
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;
@@ -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, chmodSync } from "node:fs";
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,28 +70,6 @@ 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
74
  export async function startDeviceLogin(server) {
101
75
  const res = await cloudFetch(server, `${server}/v1/auth/device`, { method: "POST" });
@@ -106,7 +80,7 @@ export async function startDeviceLogin(server) {
106
80
  }
107
81
  return (await res.json());
108
82
  }
109
- /** Poll until the device is approved in the browser. Saves the token on success. */
83
+ /** Poll until the device is approved in the browser. Returns the token (caller must save it). */
110
84
  export async function pollDeviceLogin(server, start) {
111
85
  const deadline = Date.now() + start.expires_in * 1000;
112
86
  for (;;) {
@@ -123,7 +97,6 @@ export async function pollDeviceLogin(server, start) {
123
97
  if (!res.ok)
124
98
  throw new Error(`login failed: HTTP ${res.status} ${await res.text()}`);
125
99
  const out = (await res.json());
126
- saveToken(server, out.token);
127
100
  return out;
128
101
  }
129
102
  }
@@ -396,7 +369,7 @@ export async function maybeAutoSync(store, repoRoot) {
396
369
  const cfg = readCloudConfig(repoRoot);
397
370
  if (!cfg)
398
371
  return null;
399
- if (!getToken(cfg.server))
372
+ if (!getToken(cfg.server, repoRoot))
400
373
  return null;
401
374
  const last = store.getMeta(AUTO_SYNC_LAST_KEY);
402
375
  if (last && Date.now() - new Date(last).getTime() < AUTO_SYNC_DEBOUNCE_MS)
@@ -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, saveToken, getToken, sync as cloudSync } from "../sync/client.js";
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
- writeCloudConfig(repoRoot, { server: serverUrl, brain: String(b.brain) });
236
- if (b.token)
237
- saveToken(serverUrl, String(b.token));
238
- json(res, 200, { ok: true, hasToken: !!getToken(serverUrl) });
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") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidimag",
3
- "version": "1.0.11",
3
+ "version": "1.0.12",
4
4
  "description": "Persistent, verified memory for AI coding agents. CLI: dim.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",