loopshouse 0.2.0 → 0.2.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 (3) hide show
  1. package/README.md +19 -19
  2. package/dist/loops.js +152 -30
  3. package/package.json +9 -3
package/README.md CHANGED
@@ -46,25 +46,25 @@ listings); ideator conversations persist per hackathon in `~/.loops/sessions/`.
46
46
 
47
47
  ## Commands
48
48
 
49
- | Group | Commands |
50
- |-------|----------|
51
- | root | `add <slug> [sponsorSlug]` (interactive setup: CLI install/update, skill into agents, auth) |
52
- | `auth` | `login`, `verify`, `status`, `logout` |
53
- | `hackathon` | `ideate` (1 credit/turn; stateful session, `--withProject`, `--new`), `session` (show/`--clear`), `submit` |
54
- | `project` | `get` (your one project for a hackathon), `create`, `update` (PATCH — only passed fields change; no project id, yours is resolved) |
55
- | `knowledge` | `query` (1 credit/query — graph-RAG evidence from a sponsor's docs) |
56
- | `artifact` | `list`, `save`, `update`, `remove` (ideation scratchpad) |
57
- | root | `credits` (remaining agent credits), `evaluate` (per-sponsor evaluator prompt; your project auto-included) |
49
+ | Group | Commands |
50
+ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------- |
51
+ | root | `add <slug> [sponsorSlug]` (interactive setup: CLI install/update, skill into agents, auth) |
52
+ | `auth` | `login`, `verify`, `status`, `logout` |
53
+ | `hackathon` | `ideate` (1 credit/turn; stateful session, `--withProject`, `--new`), `session` (show/`--clear`), `submit` |
54
+ | `project` | `get` (your one project for a hackathon), `create`, `update` (PATCH — only passed fields change; no project id, yours is resolved) |
55
+ | `knowledge` | `query` (1 credit/query — graph-RAG evidence from a sponsor's docs) |
56
+ | `artifact` | `list`, `save`, `update`, `remove` (ideation scratchpad) |
57
+ | root | `credits` (remaining agent credits), `evaluate` (per-sponsor evaluator prompt; your project auto-included) |
58
58
 
59
59
  ## Authentication
60
60
 
61
61
  Auth uses Supabase Auth against the Loops platform. Three flows:
62
62
 
63
- | Flow | Command |
64
- |------|---------|
65
- | GitHub OAuth | `loops auth login --provider github` |
66
- | Google OAuth | `loops auth login --provider google` |
67
- | Email OTP | `loops auth login --email you@x.com` then `loops auth verify --email you@x.com --code 123456` |
63
+ | Flow | Command |
64
+ | ------------ | --------------------------------------------------------------------------------------------- |
65
+ | GitHub OAuth | `loops auth login --provider github` |
66
+ | Google OAuth | `loops auth login --provider google` |
67
+ | Email OTP | `loops auth login --email you@x.com` then `loops auth verify --email you@x.com --code 123456` |
68
68
 
69
69
  OAuth runs a localhost callback server (default port `54321`, `--port` to
70
70
  change), opens your browser, and exchanges the PKCE code for a session.
@@ -82,11 +82,11 @@ loops --llms # print the machine-readable command manifest
82
82
 
83
83
  ## Configuration
84
84
 
85
- | Env var | Default | Purpose |
86
- |---------|---------|---------|
87
- | `LOOPS_PLATFORM_URL` | `https://loops-platform.vercel.app` | Platform API origin |
88
- | `LOOPS_SUPABASE_URL` | (baked) | Supabase project URL |
89
- | `LOOPS_SUPABASE_ANON_KEY` | (baked) | Supabase anon key |
85
+ | Env var | Default | Purpose |
86
+ | ------------------------- | ----------------------------------- | -------------------- |
87
+ | `LOOPS_PLATFORM_URL` | `https://loops-platform.vercel.app` | Platform API origin |
88
+ | `LOOPS_SUPABASE_URL` | (baked) | Supabase project URL |
89
+ | `LOOPS_SUPABASE_ANON_KEY` | (baked) | Supabase anon key |
90
90
 
91
91
  Point these at `http://localhost:3000` + your local Supabase to develop against
92
92
  a local stack.
package/dist/loops.js CHANGED
@@ -50976,10 +50976,10 @@ if (shouldShowDeprecationWarning())
50976
50976
  import os4 from "node:os";
50977
50977
  import path4 from "node:path";
50978
50978
  import fs4 from "node:fs";
50979
- var SUPABASE_URL = process.env.LOOPS_SUPABASE_URL ?? "https://ennlvrjxpqexvgfhfiaw.supabase.co";
50979
+ var SUPABASE_URL = process.env.LOOPS_SUPABASE_URL ?? "https://api.loops.house";
50980
50980
  var SUPABASE_ANON_KEY = process.env.LOOPS_SUPABASE_ANON_KEY ?? "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImVubmx2cmp4cHFleHZnZmhmaWF3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzgwOTY3NTEsImV4cCI6MjA5MzY3Mjc1MX0.chKog_0lD39fqQi7R8pwhvTLWxEdwNidS-_BFtlknXE";
50981
- var VERSION = "0.2.0";
50982
- var PLATFORM_URL = (process.env.LOOPS_PLATFORM_URL ?? "https://www.loops.house").replace(/\/+$/, "");
50981
+ var VERSION = "0.2.2";
50982
+ var PLATFORM_URL = (process.env.LOOPS_PLATFORM_URL ?? "https://loops.house").replace(/\/+$/, "");
50983
50983
  var CONFIG_DIR = path4.join(os4.homedir(), ".loops");
50984
50984
  var CREDENTIALS_PATH = path4.join(CONFIG_DIR, "credentials.json");
50985
50985
  function loadCredentials() {
@@ -51052,7 +51052,11 @@ var DEFAULT_CALLBACK_PORT = 54321;
51052
51052
  function openBrowser(url2) {
51053
51053
  const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
51054
51054
  try {
51055
- const child = spawn(cmd, [url2], { stdio: "ignore", detached: true, shell: process.platform === "win32" });
51055
+ const child = spawn(cmd, [url2], {
51056
+ stdio: "ignore",
51057
+ detached: true,
51058
+ shell: process.platform === "win32"
51059
+ });
51056
51060
  child.on("error", () => {});
51057
51061
  child.unref();
51058
51062
  } catch {}
@@ -51104,6 +51108,11 @@ function callbackPage(ok, detail) {
51104
51108
  }
51105
51109
  function waitForOAuthCallback(port) {
51106
51110
  return new Promise((resolve3) => {
51111
+ const finish = (result) => {
51112
+ clearTimeout(timer);
51113
+ server.close();
51114
+ resolve3(result);
51115
+ };
51107
51116
  const server = createServer((req, res) => {
51108
51117
  const reqUrl = new URL(req.url ?? "/", `http://localhost:${port}`);
51109
51118
  if (reqUrl.pathname !== "/callback") {
@@ -51112,18 +51121,19 @@ function waitForOAuthCallback(port) {
51112
51121
  }
51113
51122
  const code = reqUrl.searchParams.get("code");
51114
51123
  const errDetail = reqUrl.searchParams.get("error_description");
51115
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(callbackPage(Boolean(code), errDetail ?? undefined));
51116
- server.close();
51124
+ res.writeHead(200, {
51125
+ "Content-Type": "text/html; charset=utf-8",
51126
+ Connection: "close"
51127
+ }).end(callbackPage(Boolean(code), errDetail ?? undefined));
51117
51128
  if (code)
51118
- resolve3({ ok: true, code });
51129
+ finish({ ok: true, code });
51119
51130
  else
51120
- resolve3({ ok: false, message: errDetail || "No authorization code received" });
51131
+ finish({ ok: false, message: errDetail || "No authorization code received" });
51121
51132
  });
51122
- server.on("error", (e) => resolve3({ ok: false, message: `Loopback server failed: ${e.message}` }));
51133
+ server.on("error", (e) => finish({ ok: false, message: `Loopback server failed: ${e.message}` }));
51123
51134
  server.listen(port);
51124
- setTimeout(() => {
51125
- server.close();
51126
- resolve3({ ok: false, message: "Timed out waiting for the browser callback (2 min)" });
51135
+ const timer = setTimeout(() => {
51136
+ finish({ ok: false, message: "Timed out waiting for the browser callback (2 min)" });
51127
51137
  }, 120000);
51128
51138
  });
51129
51139
  }
@@ -51177,13 +51187,55 @@ async function oauthLogin(provider, port, onUrl) {
51177
51187
  });
51178
51188
  return { email: sess.user?.email, userId: sess.user?.id };
51179
51189
  }
51190
+ async function linkOAuthIdentity(provider, port, onUrl) {
51191
+ const creds = loadCredentials();
51192
+ if (!creds)
51193
+ throw new Error("Not signed in — run `loops auth login` first.");
51194
+ const supabase = createSupabase();
51195
+ const { error: sessErr } = await supabase.auth.setSession({
51196
+ access_token: creds.access_token,
51197
+ refresh_token: creds.refresh_token
51198
+ });
51199
+ if (sessErr)
51200
+ throw new Error(`Session expired — run \`loops auth login\` first (${sessErr.message})`);
51201
+ const redirectTo = `http://localhost:${port}/callback`;
51202
+ const { data, error: error51 } = await supabase.auth.linkIdentity({
51203
+ provider,
51204
+ options: {
51205
+ redirectTo,
51206
+ skipBrowserRedirect: true,
51207
+ queryParams: { prompt: "select_account" }
51208
+ }
51209
+ });
51210
+ if (error51 || !data?.url)
51211
+ throw new Error(error51?.message ?? "Failed to start identity linking");
51212
+ const callback = waitForOAuthCallback(port);
51213
+ onUrl?.(data.url);
51214
+ openBrowser(data.url);
51215
+ const result = await callback;
51216
+ if (!result.ok)
51217
+ throw new Error(result.message);
51218
+ const { data: sess, error: exErr } = await supabase.auth.exchangeCodeForSession(result.code);
51219
+ if (exErr || !sess.session)
51220
+ throw new Error(exErr?.message ?? "Code exchange failed");
51221
+ saveCredentials({
51222
+ access_token: sess.session.access_token,
51223
+ refresh_token: sess.session.refresh_token,
51224
+ expires_at: sess.session.expires_at
51225
+ });
51226
+ return {
51227
+ email: sess.user?.email,
51228
+ userId: sess.user?.id,
51229
+ identities: (sess.user?.identities ?? []).map((i) => i.provider)
51230
+ };
51231
+ }
51180
51232
  var auth = exports_Cli.create("auth", {
51181
51233
  description: "Authenticate with the Loops House platform"
51182
51234
  });
51183
51235
  auth.command("login", {
51184
51236
  description: "Log in via browser OAuth (GitHub/Google) or an email one-time code",
51185
51237
  options: exports_external.object({
51186
- provider: exports_external.enum(["github", "google"]).optional().describe("OAuth provider (default github). Ignored when --email is set."),
51238
+ provider: exports_external.enum(["github", "google"]).optional().describe("OAuth provider (default google). Ignored when --email is set."),
51187
51239
  email: exports_external.string().email().optional().describe("Log in with an email one-time code instead of a browser"),
51188
51240
  port: exports_external.number().default(DEFAULT_CALLBACK_PORT).describe("Local port for the OAuth callback server")
51189
51241
  }),
@@ -51195,8 +51247,8 @@ auth.command("login", {
51195
51247
  message: exports_external.string().optional()
51196
51248
  }),
51197
51249
  examples: [
51198
- { options: { provider: "github" }, description: "Log in with GitHub" },
51199
51250
  { options: { provider: "google" }, description: "Log in with Google" },
51251
+ { options: { provider: "github" }, description: "Log in with GitHub" },
51200
51252
  { options: { email: "you@example.com" }, description: "Send an email one-time code" }
51201
51253
  ],
51202
51254
  async run({ options, error: error51, agent }) {
@@ -51212,7 +51264,11 @@ auth.command("login", {
51212
51264
  const res = await verifyOtp(options.email, code);
51213
51265
  return { status: "authenticated", ...res };
51214
51266
  } catch (e) {
51215
- return error51({ code: "OTP_VERIFY_FAILED", message: e.message, retryable: true });
51267
+ return error51({
51268
+ code: "OTP_VERIFY_FAILED",
51269
+ message: e.message,
51270
+ retryable: true
51271
+ });
51216
51272
  }
51217
51273
  }
51218
51274
  return {
@@ -51221,7 +51277,7 @@ auth.command("login", {
51221
51277
  message: `Code sent to ${options.email}. Verify with: loops auth verify --email ${options.email} --code <code>`
51222
51278
  };
51223
51279
  }
51224
- const provider = options.provider ?? "github";
51280
+ const provider = options.provider ?? "google";
51225
51281
  try {
51226
51282
  const res = await oauthLogin(provider, options.port, (url2) => {
51227
51283
  if (!agent) {
@@ -51250,7 +51306,10 @@ auth.command("verify", {
51250
51306
  userId: exports_external.string().optional()
51251
51307
  }),
51252
51308
  examples: [
51253
- { options: { email: "you@example.com", code: "123456" }, description: "Verify the emailed code" }
51309
+ {
51310
+ options: { email: "you@example.com", code: "123456" },
51311
+ description: "Verify the emailed code"
51312
+ }
51254
51313
  ],
51255
51314
  async run({ options, error: error51 }) {
51256
51315
  try {
@@ -51261,6 +51320,41 @@ auth.command("verify", {
51261
51320
  }
51262
51321
  }
51263
51322
  });
51323
+ auth.command("link", {
51324
+ description: "Attach a GitHub/Google identity to the account you're signed in to. Use when provider sign-in fails with 'multiple accounts with the same email' — after linking, that provider always signs in to THIS account.",
51325
+ options: exports_external.object({
51326
+ provider: exports_external.enum(["github", "google"]).describe("OAuth provider identity to link"),
51327
+ port: exports_external.number().default(DEFAULT_CALLBACK_PORT).describe("Local port for the OAuth callback server")
51328
+ }),
51329
+ alias: { provider: "p" },
51330
+ output: exports_external.object({
51331
+ status: exports_external.string(),
51332
+ email: exports_external.string().optional(),
51333
+ userId: exports_external.string().optional(),
51334
+ identities: exports_external.array(exports_external.string())
51335
+ }),
51336
+ examples: [
51337
+ {
51338
+ options: { provider: "github" },
51339
+ description: "Link your GitHub identity to the signed-in account"
51340
+ }
51341
+ ],
51342
+ async run({ options, error: error51, agent }) {
51343
+ try {
51344
+ const res = await linkOAuthIdentity(options.provider, options.port, (url2) => {
51345
+ if (!agent) {
51346
+ console.error(`Opening browser to link your ${options.provider} identity…`);
51347
+ console.error(`If it doesn't open, visit:
51348
+ ${url2}
51349
+ `);
51350
+ }
51351
+ });
51352
+ return { status: "linked", ...res };
51353
+ } catch (e) {
51354
+ return error51({ code: "LINK_FAILED", message: e.message, retryable: true });
51355
+ }
51356
+ }
51357
+ });
51264
51358
  auth.command("status", {
51265
51359
  description: "Show the current authentication status",
51266
51360
  output: exports_external.object({
@@ -51293,7 +51387,11 @@ var NOT_AUTHENTICATED = {
51293
51387
  description: "To authenticate:",
51294
51388
  commands: [
51295
51389
  { command: "auth login", description: "Log in via GitHub or Google (browser)" },
51296
- { command: "auth login", options: { email: true }, description: "Log in with an email one-time code" }
51390
+ {
51391
+ command: "auth login",
51392
+ options: { email: true },
51393
+ description: "Log in with an email one-time code"
51394
+ }
51297
51395
  ]
51298
51396
  }
51299
51397
  };
@@ -51491,7 +51589,10 @@ hackathon.command("ideate", {
51491
51589
  output: exports_external.object({ response: exports_external.string(), sessionTurns: exports_external.number() }),
51492
51590
  examples: [
51493
51591
  {
51494
- options: { hackathonSlug: "my-hackathon", message: "Suggest a project using the sponsor APIs" },
51592
+ options: {
51593
+ hackathonSlug: "my-hackathon",
51594
+ message: "Suggest a project using the sponsor APIs"
51595
+ },
51495
51596
  description: "Ask the mentor for an idea"
51496
51597
  },
51497
51598
  {
@@ -51560,7 +51661,11 @@ hackathon.command("submit", {
51560
51661
  output: submissionOutput,
51561
51662
  examples: [
51562
51663
  {
51563
- options: { hackathonSlug: "my-hackathon", name: "My Project", repoUrl: "https://github.com/me/proj" },
51664
+ options: {
51665
+ hackathonSlug: "my-hackathon",
51666
+ name: "My Project",
51667
+ repoUrl: "https://github.com/me/proj"
51668
+ },
51564
51669
  description: "Submit a project"
51565
51670
  }
51566
51671
  ],
@@ -51612,9 +51717,7 @@ project.command("get", {
51612
51717
  exists: exports_external.boolean(),
51613
51718
  project: projectShape.nullable()
51614
51719
  }),
51615
- examples: [
51616
- { options: { hackathonSlug: "my-hackathon" }, description: "Look up your project" }
51617
- ],
51720
+ examples: [{ options: { hackathonSlug: "my-hackathon" }, description: "Look up your project" }],
51618
51721
  run: authed(async ({ options, session, ok }) => {
51619
51722
  const result = await apiGet(session.accessToken, `/api/v1/hackathons/${options.hackathonSlug}/my-project`);
51620
51723
  return ok(result, {
@@ -51699,7 +51802,9 @@ knowledge.command("query", {
51699
51802
  hackathonSlug: exports_external.string().describe("Hackathon slug the sponsor belongs to"),
51700
51803
  sponsorSlug: exports_external.string().describe("Sponsor slug whose knowledge graph to query"),
51701
51804
  query: exports_external.string().describe("A focused question about the sponsor's products, docs, or SDKs"),
51702
- mode: exports_external.enum(["mix", "local", "global", "hybrid", "naive", "bypass"]).optional().describe("'mix' = balanced (default), 'local' = entity-centric, 'global' = relation-centric")
51805
+ mode: exports_external.enum(["mix", "local", "global", "hybrid", "naive", "bypass"]).optional().describe("'mix' = balanced (default), 'local' = entity-centric, 'global' = relation-centric"),
51806
+ hlKeywords: exports_external.array(exports_external.string()).optional().describe("2-5 high-level concept keywords — supplying them skips a server-side LLM call"),
51807
+ llKeywords: exports_external.array(exports_external.string()).optional().describe("2-5 specific low-level terms (APIs, product names) that focus retrieval")
51703
51808
  }),
51704
51809
  alias: { sponsorSlug: "s", query: "q" },
51705
51810
  output: exports_external.object({ evidence: exports_external.string() }),
@@ -51713,7 +51818,12 @@ knowledge.command("query", {
51713
51818
  description: "Ask about a sponsor's SDK"
51714
51819
  }
51715
51820
  ],
51716
- run: authed(({ options, session }) => apiPost(session.accessToken, `/api/v1/hackathons/${options.hackathonSlug}/sponsors/${options.sponsorSlug}/knowledge/query`, { query: options.query, mode: options.mode }))
51821
+ run: authed(({ options, session }) => apiPost(session.accessToken, `/api/v1/hackathons/${options.hackathonSlug}/sponsors/${options.sponsorSlug}/knowledge/query`, {
51822
+ query: options.query,
51823
+ mode: options.mode,
51824
+ hlKeywords: options.hlKeywords,
51825
+ llKeywords: options.llKeywords
51826
+ }))
51717
51827
  });
51718
51828
 
51719
51829
  // src/artifact.ts
@@ -53496,7 +53606,11 @@ var TARGETS = {
53496
53606
  };
53497
53607
  var ALL_TARGETS = Object.keys(TARGETS);
53498
53608
  function npm(args, timeout = 60000) {
53499
- const r2 = spawnSync("npm", args, { encoding: "utf8", timeout, shell: process.platform === "win32" });
53609
+ const r2 = spawnSync("npm", args, {
53610
+ encoding: "utf8",
53611
+ timeout,
53612
+ shell: process.platform === "win32"
53613
+ });
53500
53614
  return { ok: r2.status === 0, stdout: r2.stdout ?? "" };
53501
53615
  }
53502
53616
  function globalCliVersion() {
@@ -53669,7 +53783,7 @@ var cli = exports_Cli.create("loops", {
53669
53783
  description: "Loops House CLI — authenticate, ideate with AI, query sponsor knowledge graphs, and submit projects from your terminal or AI agent. Scoped to one hackathon at a time.",
53670
53784
  sync: {
53671
53785
  suggestions: [
53672
- "log in to loops with github",
53786
+ "log in to loops with google",
53673
53787
  "ideate a project for a hackathon",
53674
53788
  "query a sponsor's knowledge graph",
53675
53789
  "evaluate my project against a sponsor"
@@ -53702,8 +53816,15 @@ cli.command("add", {
53702
53816
  }),
53703
53817
  examples: [
53704
53818
  { args: { hackathonSlug: "my-hackathon" }, description: "Interactive setup for a hackathon" },
53705
- { args: { hackathonSlug: "my-hackathon", sponsorSlug: "acme" }, description: "Install Acme's sponsor-focused skill" },
53706
- { args: { hackathonSlug: "my-hackathon" }, options: { yes: true }, description: "Non-interactive, defaults" }
53819
+ {
53820
+ args: { hackathonSlug: "my-hackathon", sponsorSlug: "acme" },
53821
+ description: "Install Acme's sponsor-focused skill"
53822
+ },
53823
+ {
53824
+ args: { hackathonSlug: "my-hackathon" },
53825
+ options: { yes: true },
53826
+ description: "Non-interactive, defaults"
53827
+ }
53707
53828
  ],
53708
53829
  run: ({ args, options, error: error51 }) => runAdd(args.hackathonSlug, args.sponsorSlug, options, error51)
53709
53830
  });
@@ -53749,4 +53870,5 @@ cli.command("evaluate", {
53749
53870
  });
53750
53871
  })
53751
53872
  });
53752
- cli.serve();
53873
+ await cli.serve();
53874
+ process.exit();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "loopshouse",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Loops House CLI — manage hackathon projects, ideate with AI, query sponsor knowledge graphs, and submit from your terminal or AI agent",
6
6
  "license": "MIT",
@@ -19,7 +19,10 @@
19
19
  "dev": "bun run bin/loops.ts",
20
20
  "build": "bun build bin/loops.ts --outdir dist --target node && node -e \"const fs=require('fs');const f='dist/loops.js';let c=fs.readFileSync(f,'utf8').replace(/^#!.*\\n/,'');fs.writeFileSync(f,'#!/usr/bin/env node\\n'+c);fs.chmodSync(f,0o755)\"",
21
21
  "typecheck": "tsc --noEmit",
22
- "prepublishOnly": "bun run build"
22
+ "prepublishOnly": "bun run build",
23
+ "lint": "eslint . --max-warnings=0",
24
+ "format": "prettier --write .",
25
+ "format:check": "prettier --check ."
23
26
  },
24
27
  "dependencies": {
25
28
  "@clack/prompts": "^1.5.1",
@@ -29,6 +32,9 @@
29
32
  },
30
33
  "devDependencies": {
31
34
  "@types/node": "^20",
32
- "typescript": "^5"
35
+ "eslint": "^10.4.1",
36
+ "prettier": "^3.8.4",
37
+ "typescript": "^5",
38
+ "typescript-eslint": "^8.61.0"
33
39
  }
34
40
  }