@pebblehouse/odin-cli 0.4.0 → 0.4.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.
- package/dist/index.js +110 -30
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -102,8 +102,13 @@ import { Command } from "commander";
|
|
|
102
102
|
|
|
103
103
|
// src/auth/refresh.ts
|
|
104
104
|
import { createClient } from "@supabase/supabase-js";
|
|
105
|
+
import { existsSync as existsSync2, writeFileSync as writeFileSync2, unlinkSync as unlinkSync2, readFileSync as readFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
106
|
+
import { dirname } from "path";
|
|
105
107
|
var SUPABASE_URL = process.env.ODIN_SUPABASE_URL ?? "https://vdiwtiiksdyhlibqrngw.supabase.co";
|
|
106
108
|
var SUPABASE_ANON_KEY = process.env.ODIN_SUPABASE_ANON_KEY ?? "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InZkaXd0aWlrc2R5aGxpYnFybmd3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzM2ODM4NDksImV4cCI6MjA4OTI1OTg0OX0.gAccODqqhBnL_npqG7H42EvaT2CWR1P2pqo5NVjKFas";
|
|
109
|
+
var REFRESH_THRESHOLD_SECONDS = 900;
|
|
110
|
+
var LOCK_PATH = CREDENTIALS_PATH + ".lock";
|
|
111
|
+
var LOCK_STALE_MS = 1e4;
|
|
107
112
|
function createNodeClient() {
|
|
108
113
|
return createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
|
|
109
114
|
auth: {
|
|
@@ -112,15 +117,46 @@ function createNodeClient() {
|
|
|
112
117
|
}
|
|
113
118
|
});
|
|
114
119
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
120
|
+
function acquireLock() {
|
|
121
|
+
try {
|
|
122
|
+
if (existsSync2(LOCK_PATH)) {
|
|
123
|
+
try {
|
|
124
|
+
const stat = JSON.parse(readFileSync2(LOCK_PATH, "utf-8"));
|
|
125
|
+
if (Date.now() - stat.pid_time > LOCK_STALE_MS) {
|
|
126
|
+
unlinkSync2(LOCK_PATH);
|
|
127
|
+
} else {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
} catch {
|
|
131
|
+
try {
|
|
132
|
+
unlinkSync2(LOCK_PATH);
|
|
133
|
+
} catch {
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const dir = dirname(LOCK_PATH);
|
|
138
|
+
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
139
|
+
writeFileSync2(LOCK_PATH, JSON.stringify({ pid: process.pid, pid_time: Date.now() }), { flag: "wx" });
|
|
140
|
+
return true;
|
|
141
|
+
} catch {
|
|
142
|
+
return false;
|
|
119
143
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
144
|
+
}
|
|
145
|
+
function releaseLock() {
|
|
146
|
+
try {
|
|
147
|
+
unlinkSync2(LOCK_PATH);
|
|
148
|
+
} catch {
|
|
123
149
|
}
|
|
150
|
+
}
|
|
151
|
+
async function waitForLock(timeoutMs = 5e3) {
|
|
152
|
+
const start = Date.now();
|
|
153
|
+
while (Date.now() - start < timeoutMs) {
|
|
154
|
+
if (acquireLock()) return true;
|
|
155
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
async function refreshTokens(creds) {
|
|
124
160
|
const supabase = createNodeClient();
|
|
125
161
|
const { data, error } = await supabase.auth.refreshSession({
|
|
126
162
|
refresh_token: creds.refresh_token
|
|
@@ -128,13 +164,42 @@ async function getValidToken() {
|
|
|
128
164
|
if (error || !data.session) {
|
|
129
165
|
throw new Error("Session expired. Run 'odin login' to re-authenticate.");
|
|
130
166
|
}
|
|
167
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
131
168
|
const newCreds = {
|
|
132
169
|
access_token: data.session.access_token,
|
|
133
170
|
refresh_token: data.session.refresh_token,
|
|
134
171
|
expires_at: data.session.expires_at ?? now + 3600
|
|
135
172
|
};
|
|
136
173
|
saveCredentials(newCreds);
|
|
137
|
-
return newCreds
|
|
174
|
+
return newCreds;
|
|
175
|
+
}
|
|
176
|
+
async function getValidToken() {
|
|
177
|
+
const creds = getCredentials();
|
|
178
|
+
if (!creds) {
|
|
179
|
+
throw new Error("Not logged in. Run 'odin login' first.");
|
|
180
|
+
}
|
|
181
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
182
|
+
if (creds.expires_at > now + REFRESH_THRESHOLD_SECONDS) {
|
|
183
|
+
return creds.access_token;
|
|
184
|
+
}
|
|
185
|
+
const locked = await waitForLock();
|
|
186
|
+
try {
|
|
187
|
+
const freshCreds = getCredentials();
|
|
188
|
+
if (freshCreds && freshCreds.expires_at > now + REFRESH_THRESHOLD_SECONDS) {
|
|
189
|
+
return freshCreds.access_token;
|
|
190
|
+
}
|
|
191
|
+
const useForRefresh = freshCreds ?? creds;
|
|
192
|
+
const newCreds = await refreshTokens(useForRefresh);
|
|
193
|
+
return newCreds.access_token;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
const retryCreds = getCredentials();
|
|
196
|
+
if (retryCreds && retryCreds.expires_at > now + REFRESH_THRESHOLD_SECONDS) {
|
|
197
|
+
return retryCreds.access_token;
|
|
198
|
+
}
|
|
199
|
+
throw err;
|
|
200
|
+
} finally {
|
|
201
|
+
if (locked) releaseLock();
|
|
202
|
+
}
|
|
138
203
|
}
|
|
139
204
|
|
|
140
205
|
// src/api-client.ts
|
|
@@ -195,7 +260,7 @@ pebblesCmd.command("update <slug>").description("Update a pebble").option("--slu
|
|
|
195
260
|
|
|
196
261
|
// src/commands/documents.ts
|
|
197
262
|
import { Command as Command2 } from "commander";
|
|
198
|
-
import { readFileSync as
|
|
263
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
199
264
|
var docsCmd = new Command2("docs").description("Manage documents");
|
|
200
265
|
docsCmd.command("list <slug>").description("List documents for a pebble").action(async (slug) => {
|
|
201
266
|
const res = await apiRequest(`/pebbles/${slug}/documents`);
|
|
@@ -208,7 +273,7 @@ docsCmd.command("get <slug> <type>").description("Get document by type").action(
|
|
|
208
273
|
if (res.error) process.exit(1);
|
|
209
274
|
});
|
|
210
275
|
docsCmd.command("create <slug> <type>").description("Create a document").requiredOption("--title <title>", "Document title").option("--file <path>", "Read content from file").option("--source <source>", "Source", "manual").action(async (slug, type, opts) => {
|
|
211
|
-
const content = opts.file ?
|
|
276
|
+
const content = opts.file ? readFileSync3(opts.file, "utf-8") : "";
|
|
212
277
|
const res = await apiRequest(`/pebbles/${slug}/documents`, {
|
|
213
278
|
method: "POST",
|
|
214
279
|
body: { doc_type: type, title: opts.title, content, source: opts.source }
|
|
@@ -224,7 +289,7 @@ docsCmd.command("delete <slug> <type>").description("Delete a document").action(
|
|
|
224
289
|
if (res.error) process.exit(1);
|
|
225
290
|
});
|
|
226
291
|
docsCmd.command("update <slug> <type>").description("Update document content").requiredOption("--file <path>", "Read content from file").option("--source <source>", "Source", "manual").action(async (slug, type, opts) => {
|
|
227
|
-
const content =
|
|
292
|
+
const content = readFileSync3(opts.file, "utf-8");
|
|
228
293
|
const res = await apiRequest(`/pebbles/${slug}/documents/${type}`, {
|
|
229
294
|
method: "PUT",
|
|
230
295
|
body: { content, source: opts.source }
|
|
@@ -307,7 +372,7 @@ var searchCmd = new Command6("search").description("Full-text search across Odin
|
|
|
307
372
|
|
|
308
373
|
// src/commands/plans.ts
|
|
309
374
|
import { Command as Command7 } from "commander";
|
|
310
|
-
import { readFileSync as
|
|
375
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
311
376
|
var plansCmd = new Command7("plans").description("Manage plans");
|
|
312
377
|
plansCmd.command("list <slug>").description("List plans for a pebble").action(async (slug) => {
|
|
313
378
|
const res = await apiRequest(`/pebbles/${slug}/plans`);
|
|
@@ -328,7 +393,7 @@ plansCmd.command("get <slug> <id>").description("Get plan with phases").action(a
|
|
|
328
393
|
process.stdout.write(JSON.stringify(result) + "\n");
|
|
329
394
|
});
|
|
330
395
|
plansCmd.command("create <slug> <title>").description("Create a plan").option("--description <description>", "Plan description").option("--file <path>", "Read description from file").option("--source <source>", "Source", "claude-code").action(async (slug, title, opts) => {
|
|
331
|
-
const description = opts.file ?
|
|
396
|
+
const description = opts.file ? readFileSync4(opts.file, "utf-8") : opts.description ?? null;
|
|
332
397
|
const res = await apiRequest(`/pebbles/${slug}/plans`, {
|
|
333
398
|
method: "POST",
|
|
334
399
|
body: { title, description, source: opts.source }
|
|
@@ -355,28 +420,24 @@ plansCmd.command("delete <slug> <id>").description("Delete a plan").action(async
|
|
|
355
420
|
process.stdout.write(JSON.stringify(res) + "\n");
|
|
356
421
|
if (res.error) process.exit(1);
|
|
357
422
|
});
|
|
358
|
-
plansCmd.command("add-phase <slug> <planId> <title>").description("Add a phase to a plan").requiredOption("--phase-number <number>", "Phase number", parseInt).option("--description <description>", "Phase description").
|
|
423
|
+
plansCmd.command("add-phase <slug> <planId> <title>").description("Add a phase to a plan").requiredOption("--phase-number <number>", "Phase number", parseInt).option("--description <description>", "Phase description").action(async (slug, planId, title, opts) => {
|
|
359
424
|
const res = await apiRequest(`/pebbles/${slug}/plans/${planId}/phases`, {
|
|
360
425
|
method: "POST",
|
|
361
426
|
body: {
|
|
362
427
|
phase_number: opts.phaseNumber,
|
|
363
428
|
title,
|
|
364
|
-
description: opts.description ?? null
|
|
365
|
-
detail_type: opts.detailType ?? null,
|
|
366
|
-
detail_id: opts.detailId ?? null
|
|
429
|
+
description: opts.description ?? null
|
|
367
430
|
}
|
|
368
431
|
});
|
|
369
432
|
process.stdout.write(JSON.stringify(res) + "\n");
|
|
370
433
|
if (res.error) process.exit(1);
|
|
371
434
|
});
|
|
372
|
-
plansCmd.command("update-phase <slug> <planId> <phaseId>").description("Update a phase").option("--title <title>", "Phase title").option("--status <status>", "Phase status (pending|in_progress|completed|skipped)").option("--notes <notes>", "Phase notes").option("--description <description>", "Phase description").
|
|
435
|
+
plansCmd.command("update-phase <slug> <planId> <phaseId>").description("Update a phase").option("--title <title>", "Phase title").option("--status <status>", "Phase status (pending|in_progress|completed|skipped)").option("--notes <notes>", "Phase notes").option("--description <description>", "Phase description").action(async (slug, planId, phaseId, opts) => {
|
|
373
436
|
const body = {};
|
|
374
437
|
if (opts.title) body.title = opts.title;
|
|
375
438
|
if (opts.status) body.status = opts.status;
|
|
376
439
|
if (opts.notes) body.notes = opts.notes;
|
|
377
440
|
if (opts.description) body.description = opts.description;
|
|
378
|
-
if (opts.detailType) body.detail_type = opts.detailType;
|
|
379
|
-
if (opts.detailId) body.detail_id = opts.detailId;
|
|
380
441
|
const res = await apiRequest(`/pebbles/${slug}/plans/${planId}/phases/${phaseId}`, {
|
|
381
442
|
method: "PUT",
|
|
382
443
|
body
|
|
@@ -391,6 +452,25 @@ plansCmd.command("delete-phase <slug> <planId> <phaseId>").description("Delete a
|
|
|
391
452
|
process.stdout.write(JSON.stringify(res) + "\n");
|
|
392
453
|
if (res.error) process.exit(1);
|
|
393
454
|
});
|
|
455
|
+
plansCmd.command("add-phase-link <slug> <planId> <phaseId>").description("Add a link from a phase to a spec or document").requiredOption("--link-type <type>", "Link type (spec|plan_doc)").requiredOption("--target-type <type>", "Target type (agent_spec|document)").requiredOption("--target-id <id>", "Target entity UUID").action(async (slug, planId, phaseId, opts) => {
|
|
456
|
+
const res = await apiRequest(`/pebbles/${slug}/plans/${planId}/phases/${phaseId}/links`, {
|
|
457
|
+
method: "POST",
|
|
458
|
+
body: {
|
|
459
|
+
link_type: opts.linkType,
|
|
460
|
+
target_type: opts.targetType,
|
|
461
|
+
target_id: opts.targetId
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
465
|
+
if (res.error) process.exit(1);
|
|
466
|
+
});
|
|
467
|
+
plansCmd.command("remove-phase-link <slug> <planId> <phaseId> <linkId>").description("Remove a link from a phase").action(async (slug, planId, phaseId, linkId) => {
|
|
468
|
+
const res = await apiRequest(`/pebbles/${slug}/plans/${planId}/phases/${phaseId}/links/${linkId}`, {
|
|
469
|
+
method: "DELETE"
|
|
470
|
+
});
|
|
471
|
+
process.stdout.write(JSON.stringify(res) + "\n");
|
|
472
|
+
if (res.error) process.exit(1);
|
|
473
|
+
});
|
|
394
474
|
|
|
395
475
|
// src/commands/conversations.ts
|
|
396
476
|
import { Command as Command8 } from "commander";
|
|
@@ -447,7 +527,7 @@ conversationsCmd.command("delete <slug> <id>").description("Delete a conversatio
|
|
|
447
527
|
|
|
448
528
|
// src/commands/specs.ts
|
|
449
529
|
import { Command as Command9 } from "commander";
|
|
450
|
-
import { readFileSync as
|
|
530
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
451
531
|
var specsCmd = new Command9("specs").description("Manage agent specs");
|
|
452
532
|
specsCmd.command("list <slug>").description("List specs for a pebble").option("--source <source>", "Filter by source (claude-code, superpowers, etc.)").action(async (slug, opts) => {
|
|
453
533
|
const params = {};
|
|
@@ -462,7 +542,7 @@ specsCmd.command("get <slug> <id>").description("Get a spec by ID").action(async
|
|
|
462
542
|
if (res.error) process.exit(1);
|
|
463
543
|
});
|
|
464
544
|
specsCmd.command("create <slug> <title>").description("Create a spec").requiredOption("--spec-type <type>", "Spec type (e.g. brainstorm, plan, research)").option("--source <source>", "Source agent", "claude-code").option("--content <content>", "Spec content").option("--file <path>", "Read content from file").option("--session-id <id>", "Link to active session").action(async (slug, title, opts) => {
|
|
465
|
-
const content = opts.file ?
|
|
545
|
+
const content = opts.file ? readFileSync5(opts.file, "utf-8") : opts.content ?? "";
|
|
466
546
|
const res = await apiRequest(`/pebbles/${slug}/agent-specs`, {
|
|
467
547
|
method: "POST",
|
|
468
548
|
body: {
|
|
@@ -481,7 +561,7 @@ specsCmd.command("update <slug> <id>").description("Update a spec").option("--ti
|
|
|
481
561
|
if (opts.title) body.title = opts.title;
|
|
482
562
|
if (opts.specType) body.spec_type = opts.specType;
|
|
483
563
|
if (opts.source) body.source = opts.source;
|
|
484
|
-
if (opts.file) body.content =
|
|
564
|
+
if (opts.file) body.content = readFileSync5(opts.file, "utf-8");
|
|
485
565
|
else if (opts.content) body.content = opts.content;
|
|
486
566
|
const res = await apiRequest(`/pebbles/${slug}/agent-specs/${id}`, {
|
|
487
567
|
method: "PUT",
|
|
@@ -553,7 +633,7 @@ versionsCmd.command("get <slug> <docType> <versionId>").description("Get a speci
|
|
|
553
633
|
|
|
554
634
|
// src/commands/guidelines.ts
|
|
555
635
|
import { Command as Command12 } from "commander";
|
|
556
|
-
import { readFileSync as
|
|
636
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
557
637
|
var guidelinesCmd = new Command12("guidelines").description("Manage global guidelines");
|
|
558
638
|
guidelinesCmd.command("list").description("List guidelines").option("--category <category>", "Filter by category").option("--status <status>", "Filter by status (default: active)").option("--all", "Include deprecated guidelines").action(async (opts) => {
|
|
559
639
|
const params = new URLSearchParams();
|
|
@@ -571,7 +651,7 @@ guidelinesCmd.command("get <id>").description("Get a guideline by ID").action(as
|
|
|
571
651
|
if (res.error) process.exit(1);
|
|
572
652
|
});
|
|
573
653
|
guidelinesCmd.command("create <title>").description("Create a guideline").option("--category <category>", "Category", "other").option("--content <content>", "Guideline content").option("--file <path>", "Read content from file").option("--source <source>", "Source", "manual").action(async (title, opts) => {
|
|
574
|
-
const content = opts.file ?
|
|
654
|
+
const content = opts.file ? readFileSync6(opts.file, "utf-8") : opts.content ?? "";
|
|
575
655
|
const res = await apiRequest("/guidelines", {
|
|
576
656
|
method: "POST",
|
|
577
657
|
body: { category: opts.category, title, content, source: opts.source }
|
|
@@ -582,7 +662,7 @@ guidelinesCmd.command("create <title>").description("Create a guideline").option
|
|
|
582
662
|
guidelinesCmd.command("update <id>").description("Update a guideline").option("--title <title>", "New title").option("--content <content>", "New content").option("--file <path>", "Read content from file").option("--category <category>", "New category").option("--status <status>", "New status (active|deprecated)").option("--source <source>", "Source").action(async (id, opts) => {
|
|
583
663
|
const body = {};
|
|
584
664
|
if (opts.title) body.title = opts.title;
|
|
585
|
-
if (opts.file) body.content =
|
|
665
|
+
if (opts.file) body.content = readFileSync6(opts.file, "utf-8");
|
|
586
666
|
else if (opts.content) body.content = opts.content;
|
|
587
667
|
if (opts.category) body.category = opts.category;
|
|
588
668
|
if (opts.status) body.status = opts.status;
|
|
@@ -661,11 +741,11 @@ observationsCmd.command("synthesize [slug]").description("Trigger observation sy
|
|
|
661
741
|
});
|
|
662
742
|
|
|
663
743
|
// src/index.ts
|
|
664
|
-
import { readFileSync as
|
|
744
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
665
745
|
import { fileURLToPath } from "url";
|
|
666
|
-
import { dirname, resolve } from "path";
|
|
667
|
-
var __dirname =
|
|
668
|
-
var pkg = JSON.parse(
|
|
746
|
+
import { dirname as dirname2, resolve } from "path";
|
|
747
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
748
|
+
var pkg = JSON.parse(readFileSync7(resolve(__dirname, "../package.json"), "utf-8"));
|
|
669
749
|
var GOLD = "\x1B[33m";
|
|
670
750
|
var DIM = "\x1B[2m";
|
|
671
751
|
var RESET = "\x1B[0m";
|