artisense 0.1.0 → 0.1.2

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 (2) hide show
  1. package/dist/index.js +169 -57
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,21 +1,54 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/commands/skill.ts
4
- import { mkdir, writeFile } from "node:fs/promises";
5
- import { resolve as resolve2, dirname } from "node:path";
3
+ // src/commands/login.ts
4
+ import { createServer } from "node:http";
5
+ import { randomBytes } from "node:crypto";
6
+ import { exec } from "node:child_process";
6
7
 
7
8
  // src/config.ts
8
9
  import { readFile } from "node:fs/promises";
9
10
  import { resolve } from "node:path";
10
- var DEFAULT_BASE_URL = "https://artisense.app";
11
- function getApiKey() {
12
- const key = process.env.ARTISENSE_API_KEY;
13
- if (!key) {
14
- console.error(`Error: ARTISENSE_API_KEY is not set.
15
- Set it in your .env or shell environment.`);
16
- process.exit(1);
11
+
12
+ // src/credentials.ts
13
+ import { readFileSync } from "node:fs";
14
+ import { writeFile, mkdir } from "node:fs/promises";
15
+ import { join, dirname } from "node:path";
16
+ import { homedir } from "node:os";
17
+ function getCredentialsPath() {
18
+ const configDir = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
19
+ return join(configDir, "artisense", "credentials.json");
20
+ }
21
+ async function saveCredentials(data) {
22
+ const filePath = getCredentialsPath();
23
+ await mkdir(dirname(filePath), { recursive: true });
24
+ await writeFile(filePath, JSON.stringify(data, null, 2), {
25
+ encoding: "utf-8",
26
+ mode: 384
27
+ });
28
+ }
29
+ function loadCredentials() {
30
+ try {
31
+ const content = readFileSync(getCredentialsPath(), "utf-8");
32
+ const parsed = JSON.parse(content);
33
+ if (parsed.apiKey && parsed.baseUrl)
34
+ return parsed;
35
+ return null;
36
+ } catch {
37
+ return null;
17
38
  }
18
- return key;
39
+ }
40
+
41
+ // src/config.ts
42
+ var DEFAULT_BASE_URL = "https://artisense.org";
43
+ function getApiKey() {
44
+ const envKey = process.env.ARTISENSE_API_KEY;
45
+ if (envKey)
46
+ return envKey;
47
+ const creds = loadCredentials();
48
+ if (creds?.apiKey)
49
+ return creds.apiKey;
50
+ console.error("Error: Not authenticated.\nRun `artisense login` or set ARTISENSE_API_KEY.");
51
+ process.exit(1);
19
52
  }
20
53
  async function getBaseUrl() {
21
54
  if (process.env.ARTISENSE_BASE_URL) {
@@ -29,10 +62,80 @@ async function getBaseUrl() {
29
62
  return match[1].trim().replace(/\/$/, "");
30
63
  }
31
64
  } catch {}
65
+ const creds = loadCredentials();
66
+ if (creds?.baseUrl)
67
+ return creds.baseUrl;
32
68
  return DEFAULT_BASE_URL;
33
69
  }
34
70
 
71
+ // src/commands/login.ts
72
+ var TIMEOUT_MS = 120000;
73
+ async function loginCommand() {
74
+ const state = randomBytes(32).toString("hex");
75
+ const baseUrl = DEFAULT_BASE_URL;
76
+ const { key } = await new Promise((resolve2, reject) => {
77
+ const server = createServer((req, res) => {
78
+ const url = new URL(req.url ?? "/", `http://localhost`);
79
+ if (url.pathname !== "/callback") {
80
+ res.writeHead(404);
81
+ res.end("Not found");
82
+ return;
83
+ }
84
+ const receivedState = url.searchParams.get("state");
85
+ const receivedKey = url.searchParams.get("key");
86
+ if (receivedState !== state) {
87
+ res.writeHead(400);
88
+ res.end("Invalid state parameter");
89
+ return;
90
+ }
91
+ if (!receivedKey) {
92
+ res.writeHead(400);
93
+ res.end("Missing key");
94
+ return;
95
+ }
96
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
97
+ res.end(`<!DOCTYPE html>
98
+ <html><body style="font-family:system-ui;display:flex;justify-content:center;align-items:center;height:100vh;margin:0">
99
+ <div style="text-align:center">
100
+ <h2>✓ Authenticated</h2>
101
+ <p>You can close this tab and return to the terminal.</p>
102
+ </div>
103
+ </body></html>`);
104
+ server.close();
105
+ resolve2({ key: receivedKey });
106
+ });
107
+ server.listen(0, "127.0.0.1", () => {
108
+ const addr = server.address();
109
+ if (!addr || typeof addr === "string") {
110
+ reject(new Error("Failed to start local server"));
111
+ return;
112
+ }
113
+ const port = addr.port;
114
+ const authUrl = `${baseUrl}/auth/cli?port=${port}&state=${state}`;
115
+ console.log("Opening browser for authentication...");
116
+ console.log(`If the browser doesn't open, visit: ${authUrl}
117
+ `);
118
+ openBrowser(authUrl);
119
+ });
120
+ const timer = setTimeout(() => {
121
+ server.close();
122
+ reject(new Error("Login timed out. Please try again."));
123
+ }, TIMEOUT_MS);
124
+ server.on("close", () => clearTimeout(timer));
125
+ });
126
+ await saveCredentials({ apiKey: key, baseUrl });
127
+ console.log("Authenticated successfully!");
128
+ console.log(`Credentials saved to ~/.config/artisense/credentials.json
129
+ `);
130
+ }
131
+ function openBrowser(url) {
132
+ const cmd = process.platform === "darwin" ? `open "${url}"` : process.platform === "win32" ? `start "${url}"` : `xdg-open "${url}"`;
133
+ exec(cmd, () => {});
134
+ }
135
+
35
136
  // src/commands/skill.ts
137
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
138
+ import { resolve as resolve2, dirname as dirname2 } from "node:path";
36
139
  var TARGETS = {
37
140
  claude: {
38
141
  path: ".claude/commands/artisense.md",
@@ -69,46 +172,39 @@ async function skillCommand(args) {
69
172
  const content = await res.text();
70
173
  const cwd = process.cwd();
71
174
  const rootPath = resolve2(cwd, "SKILL.md");
72
- await writeFile(rootPath, content, "utf-8");
175
+ await writeFile2(rootPath, content, "utf-8");
73
176
  console.log(` Project root: ${rootPath}`);
74
- for (const key of targetKeys) {
177
+ const writeResults = await Promise.all(targetKeys.map(async (key) => {
75
178
  const target = TARGETS[key];
76
179
  const fullPath = resolve2(cwd, target.path);
77
180
  try {
78
- await mkdir(dirname(fullPath), { recursive: true });
79
- await writeFile(fullPath, target.wrap(content), "utf-8");
80
- console.log(` ${target.label}: ${fullPath}`);
181
+ await mkdir2(dirname2(fullPath), { recursive: true });
182
+ await writeFile2(fullPath, target.wrap(content), "utf-8");
183
+ return ` ${target.label}: ${fullPath}`;
81
184
  } catch {
82
- console.warn(` ${target.label}: skipped (cannot write)`);
185
+ return ` ${target.label}: skipped (cannot write)`;
83
186
  }
84
- }
187
+ }));
188
+ writeResults.forEach((msg) => console.log(msg));
85
189
  console.log(`
86
190
  Next: set your API key:
87
191
  ARTISENSE_API_KEY="ask_..."
88
192
  `);
89
193
  }
90
194
  function parseArgs(args) {
91
- let baseUrl = DEFAULT_BASE_URL;
92
- const targetKeys = [];
93
- for (const arg of args) {
94
- if (arg === "--help" || arg === "-h") {
95
- printUsage();
96
- process.exit(0);
97
- } else if (arg === "--all") {
98
- targetKeys.push(...ALL_TARGET_KEYS);
99
- } else if (arg.startsWith("--")) {
100
- const key = arg.slice(2);
101
- if (key in TARGETS) {
102
- targetKeys.push(key);
103
- } else {
104
- console.error(`Unknown option: ${arg}`);
105
- printUsage();
106
- process.exit(1);
107
- }
108
- } else {
109
- baseUrl = arg.replace(/\/$/, "");
110
- }
195
+ if (args.includes("--help") || args.includes("-h")) {
196
+ printUsage();
197
+ process.exit(0);
198
+ }
199
+ const unknownFlag = args.find((arg) => arg.startsWith("--") && arg !== "--all" && !(arg.slice(2) in TARGETS));
200
+ if (unknownFlag) {
201
+ console.error(`Unknown option: ${unknownFlag}`);
202
+ printUsage();
203
+ process.exit(1);
111
204
  }
205
+ const targetKeys = args.includes("--all") ? ALL_TARGET_KEYS : args.filter((arg) => arg.startsWith("--") && (arg.slice(2) in TARGETS)).map((arg) => arg.slice(2));
206
+ const baseUrlArg = args.find((arg) => !arg.startsWith("--"));
207
+ const baseUrl = baseUrlArg ? baseUrlArg.replace(/\/$/, "") : DEFAULT_BASE_URL;
112
208
  return { baseUrl, targetKeys };
113
209
  }
114
210
  function printUsage() {
@@ -175,29 +271,34 @@ async function documentsCommand(args) {
175
271
  }
176
272
  }
177
273
  async function listDocuments() {
178
- const [baseUrl, apiKey] = await Promise.all([getBaseUrl(), getApiKey()]);
274
+ const apiKey = getApiKey();
275
+ const baseUrl = await getBaseUrl();
179
276
  const result = await request(baseUrl, "/api/v1/documents", apiKey);
180
277
  if (result.data.length === 0) {
181
278
  console.log("No documents found.");
182
279
  return;
183
280
  }
184
- for (const doc of result.data) {
281
+ const lines = result.data.map((doc) => {
185
282
  const title = doc.title || "(Untitled)";
186
- console.log(`${doc.id} ${title} [${doc.type}] ${doc.updatedAt}`);
187
- }
283
+ return `${doc.id} ${title} [${doc.type}] ${doc.updatedAt}`;
284
+ });
285
+ console.log(lines.join(`
286
+ `));
188
287
  }
189
288
  async function getDocument(id) {
190
289
  if (!id) {
191
290
  console.error("Usage: artisense docs get <document-id>");
192
291
  process.exit(1);
193
292
  }
194
- const [baseUrl, apiKey] = await Promise.all([getBaseUrl(), getApiKey()]);
293
+ const apiKey = getApiKey();
294
+ const baseUrl = await getBaseUrl();
195
295
  const result = await request(baseUrl, `/api/v1/documents/${id}`, apiKey);
196
296
  console.log(JSON.stringify(result.data, null, 2));
197
297
  }
198
298
  async function createDocument(args) {
199
299
  const body = parseFlags(args, ["title", "type", "parentId"]);
200
- const [baseUrl, apiKey] = await Promise.all([getBaseUrl(), getApiKey()]);
300
+ const apiKey = getApiKey();
301
+ const baseUrl = await getBaseUrl();
201
302
  const result = await request(baseUrl, "/api/v1/documents", apiKey, {
202
303
  method: "POST",
203
304
  body
@@ -214,8 +315,12 @@ async function updateDocument(id, args) {
214
315
  console.error("Provide at least one of: --title, --content");
215
316
  process.exit(1);
216
317
  }
217
- const [baseUrl, apiKey] = await Promise.all([getBaseUrl(), getApiKey()]);
218
- const result = await request(baseUrl, `/api/v1/documents/${id}`, apiKey, { method: "PATCH", body });
318
+ const apiKey = getApiKey();
319
+ const baseUrl = await getBaseUrl();
320
+ const result = await request(baseUrl, `/api/v1/documents/${id}`, apiKey, {
321
+ method: "PATCH",
322
+ body
323
+ });
219
324
  console.log(`Updated: ${result.data.id} ${result.data.title || "(Untitled)"}`);
220
325
  }
221
326
  async function deleteDocument(id) {
@@ -223,24 +328,29 @@ async function deleteDocument(id) {
223
328
  console.error("Usage: artisense docs delete <document-id>");
224
329
  process.exit(1);
225
330
  }
226
- const [baseUrl, apiKey] = await Promise.all([getBaseUrl(), getApiKey()]);
331
+ const apiKey = getApiKey();
332
+ const baseUrl = await getBaseUrl();
227
333
  await request(baseUrl, `/api/v1/documents/${id}`, apiKey, {
228
334
  method: "DELETE"
229
335
  });
230
336
  console.log(`Deleted: ${id}`);
231
337
  }
232
338
  function parseFlags(args, allowedKeys) {
233
- const result = {};
234
- for (let i = 0;i < args.length; i++) {
235
- const arg = args[i];
339
+ return args.reduce((acc, arg) => {
340
+ if (acc.pendingKey) {
341
+ return {
342
+ result: { ...acc.result, [acc.pendingKey]: arg },
343
+ pendingKey: null
344
+ };
345
+ }
236
346
  if (arg.startsWith("--")) {
237
347
  const key = arg.slice(2);
238
- if (allowedKeys.includes(key) && i + 1 < args.length) {
239
- result[key] = args[++i];
348
+ if (allowedKeys.includes(key)) {
349
+ return { ...acc, pendingKey: key };
240
350
  }
241
351
  }
242
- }
243
- return result;
352
+ return acc;
353
+ }, { result: {}, pendingKey: null }).result;
244
354
  }
245
355
  function printUsage2() {
246
356
  console.log(`
@@ -261,6 +371,9 @@ Commands:
261
371
  var args = process.argv.slice(2);
262
372
  var command = args[0];
263
373
  switch (command) {
374
+ case "login":
375
+ await loginCommand();
376
+ break;
264
377
  case "skill":
265
378
  await skillCommand(args.slice(1));
266
379
  break;
@@ -286,14 +399,13 @@ artisense — CLI for Artisense
286
399
  Usage: artisense <command>
287
400
 
288
401
  Commands:
402
+ login Authenticate via browser
289
403
  skill Download SKILL.md for AI agents
290
404
  docs Manage documents (list, get, create, update, delete)
291
405
  help Show this help
292
406
 
293
- Environment:
294
- ARTISENSE_API_KEY Your API key (required for docs commands)
295
-
296
407
  Examples:
408
+ artisense login
297
409
  artisense skill --claude
298
410
  artisense docs list
299
411
  artisense docs get <id>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artisense",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI for Artisense — manage documents from your terminal or AI agent",
5
5
  "license": "MIT",
6
6
  "bin": {