@staff0rd/assist 0.62.0 → 0.63.0

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 +1 -1
  2. package/dist/index.js +208 -30
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -57,7 +57,7 @@ After installation, the `assist` command will be available globally.
57
57
  - `assist backlog add` - Add a new backlog item interactively
58
58
  - `assist backlog start <id>` - Set a backlog item to in-progress
59
59
  - `assist backlog done <id>` - Set a backlog item to done
60
- - `assist roam auth` - Configure Roam API credentials (saved to ~/.assist.yml)
60
+ - `assist roam auth` - Authenticate with Roam via OAuth (opens browser, saves tokens to ~/.assist.yml)
61
61
  - `assist run <name>` - Run a configured command from assist.yml
62
62
  - `assist run add` - Add a new run configuration to assist.yml
63
63
  - `assist config set <key> <value>` - Set a config value (e.g. commit.push true)
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { Command } from "commander";
6
6
  // package.json
7
7
  var package_default = {
8
8
  name: "@staff0rd/assist",
9
- version: "0.62.0",
9
+ version: "0.63.0",
10
10
  type: "module",
11
11
  main: "dist/index.js",
12
12
  bin: {
@@ -112,7 +112,10 @@ var assistConfigSchema = z.strictObject({
112
112
  }).optional(),
113
113
  roam: z.strictObject({
114
114
  clientId: z.string(),
115
- clientSecret: z.string()
115
+ clientSecret: z.string(),
116
+ accessToken: z.string().optional(),
117
+ refreshToken: z.string().optional(),
118
+ tokenExpiresAt: z.number().optional()
116
119
  }).optional(),
117
120
  run: z.array(runConfigSchema).optional(),
118
121
  transcript: transcriptConfigSchema.optional()
@@ -4111,9 +4114,9 @@ function addChildMoves(moves, children, newDir, parentBase) {
4111
4114
  for (const child of children)
4112
4115
  moves.push(childMoveData(child, newDir, parentBase));
4113
4116
  }
4114
- function checkDirConflict(result, label, dir) {
4117
+ function checkDirConflict(result, label2, dir) {
4115
4118
  if (!fs20.existsSync(dir)) return false;
4116
- result.warnings.push(`Skipping ${label}: directory ${dir} already exists`);
4119
+ result.warnings.push(`Skipping ${label2}: directory ${dir} already exists`);
4117
4120
  return true;
4118
4121
  }
4119
4122
  function getBaseName(filePath) {
@@ -4266,8 +4269,8 @@ function askQuestion(rl, question) {
4266
4269
  }
4267
4270
 
4268
4271
  // src/commands/transcript/configure.ts
4269
- function buildPrompt(label, current) {
4270
- return current ? `${label} [${current}]: ` : `${label}: `;
4272
+ function buildPrompt(label2, current) {
4273
+ return current ? `${label2} [${current}]: ` : `${label2}: `;
4271
4274
  }
4272
4275
  function printExisting(existing) {
4273
4276
  console.log("Current configuration:");
@@ -4618,10 +4621,10 @@ function logSkipped(relativeDir, mdFile) {
4618
4621
  console.log(`Skipping (already exists): ${join18(relativeDir, mdFile)}`);
4619
4622
  return "skipped";
4620
4623
  }
4621
- function ensureDirectory(dir, label) {
4624
+ function ensureDirectory(dir, label2) {
4622
4625
  if (!existsSync19(dir)) {
4623
4626
  mkdirSync5(dir, { recursive: true });
4624
- console.log(`Created ${label}: ${dir}`);
4627
+ console.log(`Created ${label2}: ${dir}`);
4625
4628
  }
4626
4629
  }
4627
4630
  function processCues(content) {
@@ -4860,28 +4863,203 @@ function registerVerify(program2) {
4860
4863
  }
4861
4864
 
4862
4865
  // src/commands/roam/auth.ts
4866
+ import { randomBytes } from "crypto";
4863
4867
  import chalk49 from "chalk";
4864
- import enquirer6 from "enquirer";
4865
- async function auth() {
4866
- const { clientId } = await enquirer6.prompt({
4867
- type: "input",
4868
- name: "clientId",
4869
- message: "Client ID:",
4870
- validate: (value) => value.trim().length > 0 || "Client ID is required"
4868
+
4869
+ // src/lib/openBrowser.ts
4870
+ import { execSync as execSync23 } from "child_process";
4871
+ function tryExec(commands) {
4872
+ for (const cmd of commands) {
4873
+ try {
4874
+ execSync23(cmd);
4875
+ return true;
4876
+ } catch {
4877
+ }
4878
+ }
4879
+ return false;
4880
+ }
4881
+ function openBrowser(url) {
4882
+ const platform = detectPlatform();
4883
+ const quoted = JSON.stringify(url);
4884
+ const commands = [];
4885
+ switch (platform) {
4886
+ case "macos":
4887
+ commands.push(
4888
+ `open -a "Google Chrome" ${quoted}`,
4889
+ `open -a "Microsoft Edge" ${quoted}`,
4890
+ `open -a "Safari" ${quoted}`
4891
+ );
4892
+ break;
4893
+ case "linux":
4894
+ commands.push(
4895
+ `google-chrome ${quoted}`,
4896
+ `chromium-browser ${quoted}`,
4897
+ `microsoft-edge ${quoted}`
4898
+ );
4899
+ break;
4900
+ case "windows":
4901
+ commands.push(`start chrome ${quoted}`, `start msedge ${quoted}`);
4902
+ break;
4903
+ case "wsl":
4904
+ commands.push(`wslview ${quoted}`);
4905
+ break;
4906
+ }
4907
+ if (!tryExec(commands)) {
4908
+ console.log(`Open this URL in Chrome, Edge, or Safari:
4909
+ ${url}`);
4910
+ }
4911
+ }
4912
+
4913
+ // src/commands/roam/waitForCallback.ts
4914
+ import { createServer } from "http";
4915
+ function respondHtml(res, status, title) {
4916
+ res.writeHead(status, { "Content-Type": "text/html" });
4917
+ res.end(
4918
+ `<html><body><h1>${title}</h1><p>You can close this tab.</p></body></html>`
4919
+ );
4920
+ }
4921
+ function extractCode(url, expectedState) {
4922
+ const error = url.searchParams.get("error");
4923
+ if (error) throw new Error(`Authorization denied: ${error}`);
4924
+ const state = url.searchParams.get("state");
4925
+ if (state !== expectedState)
4926
+ throw new Error("State mismatch \u2014 possible CSRF attack");
4927
+ const code = url.searchParams.get("code");
4928
+ if (!code) throw new Error("No authorization code received");
4929
+ return code;
4930
+ }
4931
+ function waitForCallback(port, expectedState) {
4932
+ return new Promise((resolve3, reject) => {
4933
+ const timeout = setTimeout(() => {
4934
+ server.close();
4935
+ reject(new Error("Authorization timed out after 120 seconds"));
4936
+ }, 12e4);
4937
+ const server = createServer((req, res) => {
4938
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
4939
+ if (url.pathname !== "/callback") {
4940
+ res.writeHead(404);
4941
+ res.end();
4942
+ return;
4943
+ }
4944
+ clearTimeout(timeout);
4945
+ try {
4946
+ const code = extractCode(url, expectedState);
4947
+ respondHtml(res, 200, "Authorization successful!");
4948
+ server.close();
4949
+ resolve3(code);
4950
+ } catch (err) {
4951
+ respondHtml(res, 400, err.message);
4952
+ server.close();
4953
+ reject(err);
4954
+ }
4955
+ });
4956
+ server.listen(port);
4957
+ });
4958
+ }
4959
+
4960
+ // src/commands/roam/authorizeInBrowser.ts
4961
+ var PORT = 14523;
4962
+ var REDIRECT_URI = `http://localhost:${PORT}/callback`;
4963
+ var SCOPES = "user:read meetings:read transcript:read user:read.email";
4964
+ function buildAuthorizeUrl(clientId, state) {
4965
+ const params = new URLSearchParams({
4966
+ client_id: clientId,
4967
+ redirect_uri: REDIRECT_URI,
4968
+ response_type: "code",
4969
+ scope: SCOPES,
4970
+ state
4971
+ });
4972
+ return `https://ro.am/oauth/authorize?${params}`;
4973
+ }
4974
+ async function authorizeInBrowser(clientId, state) {
4975
+ openBrowser(buildAuthorizeUrl(clientId, state));
4976
+ const code = await waitForCallback(PORT, state);
4977
+ return { code, redirectUri: REDIRECT_URI };
4978
+ }
4979
+
4980
+ // src/commands/roam/exchangeToken.ts
4981
+ async function exchangeToken(params) {
4982
+ const body = new URLSearchParams({
4983
+ grant_type: "authorization_code",
4984
+ code: params.code,
4985
+ client_id: params.clientId,
4986
+ client_secret: params.clientSecret,
4987
+ redirect_uri: params.redirectUri
4988
+ });
4989
+ const response = await fetch("https://ro.am/oauth/token", {
4990
+ method: "POST",
4991
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
4992
+ body: body.toString()
4871
4993
  });
4872
- const { clientSecret } = await enquirer6.prompt({
4994
+ if (!response.ok) {
4995
+ const text = await response.text();
4996
+ throw new Error(`Token exchange failed (${response.status}): ${text}`);
4997
+ }
4998
+ return response.json();
4999
+ }
5000
+
5001
+ // src/commands/roam/promptCredentials.ts
5002
+ import enquirer6 from "enquirer";
5003
+ function censor(value) {
5004
+ const visible = value.slice(-4);
5005
+ return `${"*".repeat(value.length - 4)}${visible}`;
5006
+ }
5007
+ function label(name, existing) {
5008
+ return existing ? `${name} (${censor(existing)})` : name;
5009
+ }
5010
+ async function promptField(name, existing) {
5011
+ const { value } = await enquirer6.prompt({
4873
5012
  type: "input",
4874
- name: "clientSecret",
4875
- message: "Client Secret:",
4876
- validate: (value) => value.trim().length > 0 || "Client Secret is required"
5013
+ name: "value",
5014
+ message: `${label(name, existing)}:`,
5015
+ validate: (v) => v.trim().length > 0 || !!existing || `${name} is required`
4877
5016
  });
5017
+ return value.trim() || existing || "";
5018
+ }
5019
+ async function promptCredentials(existing) {
5020
+ const clientId = await promptField("Client ID", existing?.clientId);
5021
+ const clientSecret = await promptField(
5022
+ "Client Secret",
5023
+ existing?.clientSecret
5024
+ );
5025
+ if (!clientId || !clientSecret) {
5026
+ throw new Error("Client ID and Client Secret are required");
5027
+ }
5028
+ return { clientId, clientSecret };
5029
+ }
5030
+
5031
+ // src/commands/roam/auth.ts
5032
+ async function auth() {
4878
5033
  const config = loadGlobalConfig();
5034
+ const { clientId, clientSecret } = await promptCredentials(config.roam);
5035
+ config.roam = { ...config.roam, clientId, clientSecret };
5036
+ saveGlobalConfig(config);
5037
+ const state = randomBytes(16).toString("hex");
5038
+ console.log(
5039
+ chalk49.yellow("\nEnsure this Redirect URI is set in your Roam OAuth app:")
5040
+ );
5041
+ console.log(chalk49.white("http://localhost:14523/callback\n"));
5042
+ console.log(chalk49.blue("Opening browser for authorization..."));
5043
+ console.log(chalk49.dim("Waiting for authorization callback..."));
5044
+ const { code, redirectUri } = await authorizeInBrowser(clientId, state);
5045
+ console.log(chalk49.dim("Exchanging code for tokens..."));
5046
+ const tokens = await exchangeToken({
5047
+ code,
5048
+ clientId,
5049
+ clientSecret,
5050
+ redirectUri
5051
+ });
4879
5052
  config.roam = {
4880
- clientId: clientId.trim(),
4881
- clientSecret: clientSecret.trim()
5053
+ clientId,
5054
+ clientSecret,
5055
+ accessToken: tokens.access_token,
5056
+ refreshToken: tokens.refresh_token,
5057
+ tokenExpiresAt: Date.now() + tokens.expires_in * 1e3
4882
5058
  };
4883
5059
  saveGlobalConfig(config);
4884
- console.log(chalk49.green("Roam credentials saved to ~/.assist.yml"));
5060
+ console.log(
5061
+ chalk49.green("Roam credentials and tokens saved to ~/.assist.yml")
5062
+ );
4885
5063
  }
4886
5064
 
4887
5065
  // src/commands/roam/registerRoam.ts
@@ -5109,7 +5287,7 @@ function syncCommands(claudeDir, targetBase) {
5109
5287
  }
5110
5288
 
5111
5289
  // src/commands/update.ts
5112
- import { execSync as execSync23 } from "child_process";
5290
+ import { execSync as execSync24 } from "child_process";
5113
5291
  import * as path30 from "path";
5114
5292
  import { fileURLToPath as fileURLToPath4 } from "url";
5115
5293
  var __filename3 = fileURLToPath4(import.meta.url);
@@ -5119,7 +5297,7 @@ function getInstallDir() {
5119
5297
  }
5120
5298
  function isGitRepo(dir) {
5121
5299
  try {
5122
- execSync23("git rev-parse --is-inside-work-tree", {
5300
+ execSync24("git rev-parse --is-inside-work-tree", {
5123
5301
  cwd: dir,
5124
5302
  stdio: "pipe"
5125
5303
  });
@@ -5130,7 +5308,7 @@ function isGitRepo(dir) {
5130
5308
  }
5131
5309
  function isGlobalNpmInstall(dir) {
5132
5310
  try {
5133
- const globalPrefix = execSync23("npm prefix -g", { stdio: "pipe" }).toString().trim();
5311
+ const globalPrefix = execSync24("npm prefix -g", { stdio: "pipe" }).toString().trim();
5134
5312
  return dir.startsWith(globalPrefix);
5135
5313
  } catch {
5136
5314
  return false;
@@ -5141,16 +5319,16 @@ async function update() {
5141
5319
  console.log(`Assist is installed at: ${installDir}`);
5142
5320
  if (isGitRepo(installDir)) {
5143
5321
  console.log("Detected git repo installation, pulling latest...");
5144
- execSync23("git pull", { cwd: installDir, stdio: "inherit" });
5322
+ execSync24("git pull", { cwd: installDir, stdio: "inherit" });
5145
5323
  console.log("Building...");
5146
- execSync23("npm run build", { cwd: installDir, stdio: "inherit" });
5324
+ execSync24("npm run build", { cwd: installDir, stdio: "inherit" });
5147
5325
  console.log("Syncing commands...");
5148
- execSync23("assist sync", { stdio: "inherit" });
5326
+ execSync24("assist sync", { stdio: "inherit" });
5149
5327
  } else if (isGlobalNpmInstall(installDir)) {
5150
5328
  console.log("Detected global npm installation, updating...");
5151
- execSync23("npm i -g @staff0rd/assist@latest", { stdio: "inherit" });
5329
+ execSync24("npm i -g @staff0rd/assist@latest", { stdio: "inherit" });
5152
5330
  console.log("Syncing commands...");
5153
- execSync23("assist sync", { stdio: "inherit" });
5331
+ execSync24("assist sync", { stdio: "inherit" });
5154
5332
  } else {
5155
5333
  console.error(
5156
5334
  "Could not determine installation method. Expected a git repo or global npm install."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staff0rd/assist",
3
- "version": "0.62.0",
3
+ "version": "0.63.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {