@craftspace/cli 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.
package/dist/index.js CHANGED
@@ -18187,6 +18187,22 @@ var TREE_KINDS = [
18187
18187
  PageKind.Artifact,
18188
18188
  PageKind.Table
18189
18189
  ];
18190
+ var PageLayer;
18191
+ (function(PageLayer2) {
18192
+ PageLayer2["Record"] = "record";
18193
+ PageLayer2["Knowledge"] = "knowledge";
18194
+ PageLayer2["Skill"] = "skill";
18195
+ })(PageLayer || (PageLayer = {}));
18196
+ var KIND_LAYER = {
18197
+ [PageKind.Page]: PageLayer.Knowledge,
18198
+ [PageKind.Decision]: PageLayer.Knowledge,
18199
+ [PageKind.Skill]: PageLayer.Skill,
18200
+ [PageKind.Meeting]: PageLayer.Record,
18201
+ [PageKind.Thread]: PageLayer.Record,
18202
+ [PageKind.Customer]: PageLayer.Record,
18203
+ [PageKind.Artifact]: PageLayer.Record,
18204
+ [PageKind.Table]: PageLayer.Record
18205
+ };
18190
18206
  var AUTHORABLE_KINDS = [PageKind.Page, PageKind.Decision, PageKind.Customer, PageKind.Skill];
18191
18207
  var PAGE_COLORS = [
18192
18208
  "slate",
@@ -18937,6 +18953,10 @@ var AUDIT_GROUPS = [
18937
18953
  AuditAction.WorkflowSecretRotated
18938
18954
  ]
18939
18955
  },
18956
+ {
18957
+ label: "MCP calls",
18958
+ actions: [AuditAction.McpToolCall, AuditAction.McpRequest]
18959
+ },
18940
18960
  {
18941
18961
  label: "Organization",
18942
18962
  actions: [
@@ -18948,6 +18968,7 @@ var AUDIT_GROUPS = [
18948
18968
  }
18949
18969
  ];
18950
18970
  var AUDIT_LISTED_ACTIONS = AUDIT_GROUPS.flatMap((group) => group.actions);
18971
+ var AUDIT_DEFAULT_ACTIONS = AUDIT_LISTED_ACTIONS.filter((action) => AUDIT_TIERS[action] !== AuditTier.Volume);
18951
18972
  var AUDIT_VERBS = {
18952
18973
  [AuditAction.AccessDenied]: "Was refused",
18953
18974
  [AuditAction.MemberSignedIn]: "Signed in",
@@ -19016,6 +19037,7 @@ var AuditEntrySchema = external_exports.object({
19016
19037
  target: AuditTargetSchema.optional(),
19017
19038
  data: external_exports.record(external_exports.string(), external_exports.unknown()),
19018
19039
  ip: external_exports.string().optional(),
19040
+ userAgent: external_exports.string().optional(),
19019
19041
  createdAt: external_exports.string()
19020
19042
  });
19021
19043
  var AuditSummarySchema = external_exports.object({
@@ -19258,6 +19280,12 @@ var CreateConnectionBodySchema = external_exports.object({
19258
19280
  // sent it, and can start a connect for that one kit and nothing else.
19259
19281
  inviteToken: external_exports.string().optional()
19260
19282
  });
19283
+ var ReplaceConnectionSecretBodySchema = external_exports.object({
19284
+ mcpUrl: external_exports.url().optional(),
19285
+ authToken: external_exports.string().min(1).optional(),
19286
+ dsn: external_exports.string().min(1).optional(),
19287
+ apiKey: external_exports.string().min(1).optional()
19288
+ });
19261
19289
  var ConnectionIdParamsSchema = external_exports.object({ id: external_exports.string() });
19262
19290
  var PendingOAuthSchema = external_exports.object({
19263
19291
  id: external_exports.string(),
@@ -19754,7 +19782,7 @@ var SaveWorkstationBodySchema = external_exports.object({
19754
19782
  var WorkstationResponseSchema = external_exports.object({ workstation: WorkstationSchema });
19755
19783
 
19756
19784
  // ../../shared/dist/machines/machine.js
19757
- var CLI_VERSION = "0.2.0";
19785
+ var CLI_VERSION = "0.2.2";
19758
19786
  var MACHINE_BEAT_INTERVAL_MS = 15e3;
19759
19787
  var MACHINE_BEAT_BUSY_INTERVAL_MS = 3e3;
19760
19788
  var MACHINE_RUN_OUTPUT_MAX = 32e3;
@@ -19874,6 +19902,7 @@ var AddMachineKeyBodySchema = external_exports.object({
19874
19902
  key: external_exports.string().min(60).max(4e3),
19875
19903
  label: external_exports.string().max(80).optional()
19876
19904
  });
19905
+ var RenameMachineBodySchema = external_exports.object({ name: external_exports.string().min(1).max(64) });
19877
19906
  var StartRunBodySchema = external_exports.object({ argv: external_exports.array(external_exports.string().max(4e3)).min(1).max(64) });
19878
19907
  var AnswerRunBodySchema = external_exports.object({ text: external_exports.string().min(1).max(4e3) });
19879
19908
  var MachineRunsResponseSchema = external_exports.object({ runs: external_exports.array(MachineRunSchema) });
@@ -20221,7 +20250,7 @@ var MemberActivityListResponse = external_exports.object({
20221
20250
  // dist/machine.js
20222
20251
  import { execFile as execFile3 } from "node:child_process";
20223
20252
  import { mkdir, readFile as readFile2, readdir, rm, writeFile } from "node:fs/promises";
20224
- import os3 from "node:os";
20253
+ import os4 from "node:os";
20225
20254
  import path4 from "node:path";
20226
20255
  import { promisify as promisify3 } from "node:util";
20227
20256
 
@@ -22091,6 +22120,7 @@ var dist_default6 = createPrompt((config2, done) => {
22091
22120
 
22092
22121
  // dist/run.js
22093
22122
  import { spawn } from "node:child_process";
22123
+ import os from "node:os";
22094
22124
  var runner = {
22095
22125
  attended(argv) {
22096
22126
  return new Promise((resolve) => {
@@ -22101,6 +22131,7 @@ var runner = {
22101
22131
  },
22102
22132
  async unattended({ work, onProgress }) {
22103
22133
  const argv = headlessArgv(work);
22134
+ const raw = argv[0] === "script";
22104
22135
  let output = "";
22105
22136
  let agentSessionId = work.agentSessionId ?? void 0;
22106
22137
  let lastText = "";
@@ -22126,10 +22157,24 @@ var runner = {
22126
22157
  }
22127
22158
  };
22128
22159
  const code = await new Promise((resolve) => {
22129
- const child = spawn(argv[0], argv.slice(1), { stdio: ["ignore", "pipe", "pipe"] });
22160
+ const child = spawn(argv[0], argv.slice(1), {
22161
+ stdio: ["ignore", "pipe", "pipe"],
22162
+ cwd: os.homedir(),
22163
+ env: {
22164
+ ...process.env,
22165
+ TERM: process.env.TERM ?? "xterm-256color",
22166
+ COLUMNS: process.env.COLUMNS ?? "120",
22167
+ LINES: process.env.LINES ?? "40"
22168
+ }
22169
+ });
22170
+ append("");
22130
22171
  child.stdout.setEncoding("utf8");
22131
22172
  child.stderr.setEncoding("utf8");
22132
22173
  child.stdout.on("data", (chunk) => {
22174
+ if (raw) {
22175
+ append(chunk);
22176
+ return;
22177
+ }
22133
22178
  pending += chunk;
22134
22179
  const lines = pending.split("\n");
22135
22180
  pending = lines.pop() ?? "";
@@ -22179,8 +22224,11 @@ function headlessArgv(work) {
22179
22224
  return [argv[0], "-p", argv.slice(1).join(" "), ...streaming];
22180
22225
  }
22181
22226
  function underTty(argv) {
22182
- return process.platform === "linux" ? ["script", "-qec", argv.join(" "), "/dev/null"] : ["script", "-q", "/dev/null", ...argv];
22227
+ const line = `stty rows ${PTY_ROWS} cols ${PTY_COLS} 2>/dev/null; exec ${argv.join(" ")}`;
22228
+ return process.platform === "linux" ? ["script", "-qec", line, "/dev/null"] : ["script", "-q", "/dev/null", "sh", "-c", line];
22183
22229
  }
22230
+ var PTY_ROWS = 40;
22231
+ var PTY_COLS = 120;
22184
22232
  function isAgent(command) {
22185
22233
  return AGENTS.has((command ?? "").split("/").pop() ?? "");
22186
22234
  }
@@ -22209,7 +22257,7 @@ function parseJson(line) {
22209
22257
 
22210
22258
  // dist/setup.js
22211
22259
  import { execFile as execFile2 } from "node:child_process";
22212
- import os2 from "node:os";
22260
+ import os3 from "node:os";
22213
22261
  import path3 from "node:path";
22214
22262
  import { promisify as promisify2 } from "node:util";
22215
22263
 
@@ -22217,12 +22265,12 @@ import { promisify as promisify2 } from "node:util";
22217
22265
  import { execFile } from "node:child_process";
22218
22266
  import net from "node:net";
22219
22267
  import { access, readFile } from "node:fs/promises";
22220
- import os from "node:os";
22268
+ import os2 from "node:os";
22221
22269
  import path2 from "node:path";
22222
22270
  import { promisify } from "node:util";
22223
22271
  var run = promisify(execFile);
22224
22272
  var probe = {
22225
- async tools({ mcpPath = path2.join(os.homedir(), ".mcp.json") } = {}) {
22273
+ async tools({ mcpPath = path2.join(os2.homedir(), ".mcp.json") } = {}) {
22226
22274
  const [brain, claude, codex, git, chrome, ssh] = await Promise.all([
22227
22275
  brainWired(mcpPath),
22228
22276
  version2({ id: "claude", command: "claude" }),
@@ -22268,7 +22316,7 @@ async function gitAndGithub() {
22268
22316
  };
22269
22317
  }
22270
22318
  async function githubAccount() {
22271
- const raw = await readFile(path2.join(os.homedir(), ".config", "gh", "hosts.yml"), "utf8").catch(() => null);
22319
+ const raw = await readFile(path2.join(os2.homedir(), ".config", "gh", "hosts.yml"), "utf8").catch(() => null);
22272
22320
  return raw === null ? null : githubUser(raw);
22273
22321
  }
22274
22322
  function githubUser(hostsYml) {
@@ -22308,7 +22356,7 @@ async function firstLine(command, args) {
22308
22356
  return line === "" ? { ran: false, why: "broken" } : { ran: true, line: line.slice(0, 200) };
22309
22357
  }
22310
22358
  function short(target) {
22311
- return target.startsWith(os.homedir()) ? `~${target.slice(os.homedir().length)}` : target;
22359
+ return target.startsWith(os2.homedir()) ? `~${target.slice(os2.homedir().length)}` : target;
22312
22360
  }
22313
22361
  var MAC_CHROME = ["/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"];
22314
22362
  var LINUX_CHROME = ["google-chrome", "google-chrome-stable", "chromium"];
@@ -22363,8 +22411,8 @@ function toolDirs() {
22363
22411
  return [
22364
22412
  path3.dirname(process.execPath),
22365
22413
  ...prefix === void 0 ? [] : [path3.join(prefix, "bin")],
22366
- path3.join(os2.homedir(), ".local", "bin"),
22367
- path3.join(os2.homedir(), ".npm-global", "bin"),
22414
+ path3.join(os3.homedir(), ".local", "bin"),
22415
+ path3.join(os3.homedir(), ".npm-global", "bin"),
22368
22416
  "/opt/homebrew/bin",
22369
22417
  "/usr/local/bin"
22370
22418
  ];
@@ -22419,7 +22467,7 @@ var RETRY_AFTER_MS = 216e5;
22419
22467
  var run3 = promisify3(execFile3);
22420
22468
  var machine = {
22421
22469
  home() {
22422
- return process.env.CRAFTSPACE_CLI_HOME ?? path4.join(os3.homedir(), ".craftspace");
22470
+ return process.env.CRAFTSPACE_CLI_HOME ?? path4.join(os4.homedir(), ".craftspace");
22423
22471
  },
22424
22472
  async login({ token, url: url2, install }) {
22425
22473
  const answer = await call({
@@ -22428,11 +22476,11 @@ var machine = {
22428
22476
  method: "POST",
22429
22477
  path: "/api/machines/login",
22430
22478
  body: {
22431
- name: os3.hostname().split(".")[0] ?? "machine",
22479
+ name: os4.hostname().split(".")[0] ?? "machine",
22432
22480
  platform: `${process.platform}-${process.arch}`,
22433
- sshUser: os3.userInfo().username,
22434
- cores: os3.cpus().length,
22435
- memoryGb: Math.max(1, Math.round(os3.totalmem() / 1024 ** 3)),
22481
+ sshUser: os4.userInfo().username,
22482
+ cores: os4.cpus().length,
22483
+ memoryGb: Math.max(1, Math.round(os4.totalmem() / 1024 ** 3)),
22436
22484
  cli: CLI_VERSION,
22437
22485
  publicKey: await ensureKey()
22438
22486
  },
@@ -22445,8 +22493,7 @@ var machine = {
22445
22493
  await installService();
22446
22494
  probed = {
22447
22495
  at: Date.now(),
22448
- tools: await setup.run({ install, rewire, say: (line) => process.stdout.write(`${line}
22449
- `) })
22496
+ tools: await setup.run({ install, rewire, say: (text) => process.stdout.write(text) })
22450
22497
  };
22451
22498
  return answer.machine;
22452
22499
  },
@@ -22495,7 +22542,7 @@ Signed in as ${signed.ownerName}. This machine is ${signed.name}.
22495
22542
  sessions: await readSessions(),
22496
22543
  runs: drainReports(),
22497
22544
  tools: await tools(),
22498
- sshUser: os3.userInfo().username
22545
+ sshUser: os4.userInfo().username
22499
22546
  },
22500
22547
  schema: MachineBeatResponseSchema
22501
22548
  });
@@ -22528,6 +22575,17 @@ Signed in as ${signed.ownerName}. This machine is ${signed.name}.
22528
22575
  return runHere(argv);
22529
22576
  return runThere({ argv, on });
22530
22577
  },
22578
+ async daemonLine() {
22579
+ if (process.env.CRAFTSPACE_NO_SERVICE === "1")
22580
+ return `not installed. Start it with: ${here()} daemon`;
22581
+ if (process.platform !== "linux")
22582
+ return BEATING;
22583
+ if (!await serviceRunning())
22584
+ return `not running. Start it with: ${here()} daemon`;
22585
+ if (!await userLingers())
22586
+ return `beating, but it stops when you log out. Run: ${LINGER_LINE}`;
22587
+ return BEATING;
22588
+ },
22531
22589
  async logout() {
22532
22590
  const config2 = await readConfig();
22533
22591
  const token = await readToken();
@@ -22736,7 +22794,7 @@ async function ensureKey() {
22736
22794
  if (existing !== null)
22737
22795
  return existing.trim();
22738
22796
  await mkdir(machine.home(), { recursive: true, mode: 448 });
22739
- await run3("ssh-keygen", ["-t", "ed25519", "-N", "", "-q", "-f", priv, "-C", `craftspace-${os3.hostname()}`]);
22797
+ await run3("ssh-keygen", ["-t", "ed25519", "-N", "", "-q", "-f", priv, "-C", `craftspace-${os4.hostname()}`]);
22740
22798
  return (await readFile2(pub, "utf8")).trim();
22741
22799
  }
22742
22800
  async function readConfig() {
@@ -22764,7 +22822,7 @@ async function readSessions() {
22764
22822
  return sessions.slice(0, MAX_REPORTED_SESSIONS);
22765
22823
  }
22766
22824
  async function writeAuthorizedKeys(keys) {
22767
- const target = path4.join(os3.homedir(), ".ssh", "authorized_keys");
22825
+ const target = path4.join(os4.homedir(), ".ssh", "authorized_keys");
22768
22826
  const current = await readFile2(target, "utf8").catch(() => "");
22769
22827
  const before = current.split(KEYS_BEGIN)[0] ?? "";
22770
22828
  const after = current.includes(KEYS_END) ? current.split(KEYS_END)[1] ?? "" : "";
@@ -22804,6 +22862,31 @@ async function writeAgentConfigs({ url: url2, token, writes }) {
22804
22862
  owned.push(target);
22805
22863
  }
22806
22864
  await writeFile(ownedPath(), JSON.stringify({ paths: owned }, null, 2));
22865
+ await acceptClaudeGates(serversIn(planned));
22866
+ }
22867
+ function serversIn(writes) {
22868
+ return writes.flatMap((write) => isPlainObject3(write.contents.mcpServers) ? Object.keys(write.contents.mcpServers) : []);
22869
+ }
22870
+ async function acceptClaudeGates(servers) {
22871
+ const target = path4.join(os4.homedir(), ".claude.json");
22872
+ const raw = await readFile2(target, "utf8").catch(() => null);
22873
+ if (raw === null)
22874
+ return;
22875
+ const parsed = JSON.parse(raw);
22876
+ const projects = isPlainObject3(parsed.projects) ? parsed.projects : {};
22877
+ const found = projects[os4.homedir()];
22878
+ const here2 = isPlainObject3(found) ? { ...found } : {};
22879
+ const enabled = /* @__PURE__ */ new Set([...Array.isArray(here2.enabledMcpjsonServers) ? here2.enabledMcpjsonServers : [], ...servers]);
22880
+ if (here2.hasTrustDialogAccepted === true && enabled.size === asArray(here2.enabledMcpjsonServers).length)
22881
+ return;
22882
+ here2.hasTrustDialogAccepted = true;
22883
+ here2.enabledMcpjsonServers = [...enabled];
22884
+ const next = { ...parsed, projects: { ...projects, [os4.homedir()]: here2 } };
22885
+ await writeFile(target, `${JSON.stringify(next, null, 2)}
22886
+ `, { mode: 384 });
22887
+ }
22888
+ function asArray(value) {
22889
+ return Array.isArray(value) ? value : [];
22807
22890
  }
22808
22891
  function mergeInto(current, incoming) {
22809
22892
  const merged = { ...current };
@@ -22846,20 +22929,19 @@ async function weCreated(target) {
22846
22929
  return backup === null;
22847
22930
  }
22848
22931
  async function installService() {
22849
- if (process.env.CRAFTSPACE_NO_SERVICE === "1") {
22850
- process.stdout.write("Skipping the service. Run the beat yourself with: craftspace daemon\n");
22932
+ if (process.env.CRAFTSPACE_NO_SERVICE === "1")
22851
22933
  return;
22852
- }
22853
22934
  if (process.platform === "linux") {
22854
- const dir = path4.join(os3.homedir(), ".config", "systemd", "user");
22935
+ const dir = path4.join(os4.homedir(), ".config", "systemd", "user");
22855
22936
  await mkdir(dir, { recursive: true });
22856
22937
  await writeFile(path4.join(dir, `${SERVICE_NAME}.service`), systemdUnit());
22938
+ await run3("loginctl", ["enable-linger", os4.userInfo().username]).catch(() => void 0);
22857
22939
  await run3("systemctl", ["--user", "daemon-reload"]).catch(() => void 0);
22858
22940
  await run3("systemctl", ["--user", "enable", "--now", SERVICE_NAME]).catch(() => void 0);
22859
22941
  return;
22860
22942
  }
22861
22943
  if (process.platform === "darwin") {
22862
- const target = path4.join(os3.homedir(), "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
22944
+ const target = path4.join(os4.homedir(), "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
22863
22945
  await mkdir(path4.dirname(target), { recursive: true });
22864
22946
  await writeFile(target, launchdPlist());
22865
22947
  await run3("launchctl", ["unload", target]).catch(() => void 0);
@@ -22869,11 +22951,11 @@ async function installService() {
22869
22951
  async function uninstallService() {
22870
22952
  if (process.platform === "linux") {
22871
22953
  await run3("systemctl", ["--user", "disable", "--now", SERVICE_NAME]).catch(() => void 0);
22872
- await rm(path4.join(os3.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`), { force: true });
22954
+ await rm(path4.join(os4.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`), { force: true });
22873
22955
  return;
22874
22956
  }
22875
22957
  if (process.platform === "darwin") {
22876
- const target = path4.join(os3.homedir(), "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
22958
+ const target = path4.join(os4.homedir(), "Library", "LaunchAgents", `${LAUNCH_LABEL}.plist`);
22877
22959
  await run3("launchctl", ["unload", target]).catch(() => void 0);
22878
22960
  await rm(target, { force: true });
22879
22961
  }
@@ -22907,11 +22989,21 @@ function launchdPlist() {
22907
22989
  </plist>
22908
22990
  `;
22909
22991
  }
22992
+ function here() {
22993
+ return `${process.execPath} ${entryPath()}`;
22994
+ }
22995
+ async function serviceRunning() {
22996
+ return await run3("systemctl", ["--user", "is-active", SERVICE_NAME]).catch(() => null) !== null;
22997
+ }
22998
+ async function userLingers() {
22999
+ const shown = await run3("loginctl", ["show-user", os4.userInfo().username, "-p", "Linger"]).catch(() => null);
23000
+ return shown?.stdout.includes("Linger=yes") === true;
23001
+ }
22910
23002
  function entryPath() {
22911
23003
  return path4.join(path4.dirname(new URL(import.meta.url).pathname), "index.js");
22912
23004
  }
22913
23005
  function expand(target) {
22914
- return target.startsWith("~/") ? path4.join(os3.homedir(), target.slice(2)) : target;
23006
+ return target.startsWith("~/") ? path4.join(os4.homedir(), target.slice(2)) : target;
22915
23007
  }
22916
23008
  function configPath() {
22917
23009
  return path4.join(machine.home(), "config.json");
@@ -22923,9 +23015,9 @@ function ownedPath() {
22923
23015
  return path4.join(machine.home(), "owned.json");
22924
23016
  }
22925
23017
  function load2() {
22926
- const cores = Math.max(1, os3.cpus().length);
22927
- const busy = Math.min(100, (os3.loadavg()[0] ?? 0) / cores * 100);
22928
- const used = (os3.totalmem() - os3.freemem()) / os3.totalmem() * 100;
23018
+ const cores = Math.max(1, os4.cpus().length);
23019
+ const busy = Math.min(100, (os4.loadavg()[0] ?? 0) / cores * 100);
23020
+ const used = (os4.totalmem() - os4.freemem()) / os4.totalmem() * 100;
22929
23021
  return { cpuPercent: Math.round(busy), memoryPercent: Math.round(used) };
22930
23022
  }
22931
23023
  function nextDelayMs(failures, busy = false) {
@@ -22949,6 +23041,8 @@ var MAX_BACKOFF_MS = 12e4;
22949
23041
  var KEYS_BEGIN = "# craftspace begin";
22950
23042
  var KEYS_END = "# craftspace end";
22951
23043
  var POLL_INTERVAL_MS = 1e3;
23044
+ var BEATING = "beating every 15s, outbound only, and keeps this set up";
23045
+ var LINGER_LINE = `sudo loginctl enable-linger ${os4.userInfo().username}`;
22952
23046
  var DEFAULT_URL = process.env.CRAFTSPACE_URL ?? "https://craftspace.app";
22953
23047
  var PROBE_EVERY_MS = 3e5;
22954
23048
  var probed = null;
@@ -22966,7 +23060,7 @@ program2.command("login").description("sign this machine in").option("--token <t
22966
23060
  `Signed in as ${signed.ownerName}`,
22967
23061
  `Machine ${signed.name} \xB7 ${signed.platform} \xB7 ${signed.cores ?? "?"} vCPU`,
22968
23062
  "MCP wired ~/.mcp.json",
22969
- "Daemon beating every 15s, outbound only, and keeps this set up",
23063
+ `Daemon ${await machine.daemonLine()}`,
22970
23064
  "",
22971
23065
  `Listed at ${url2.replace(/\/$/, "")}/workstations`,
22972
23066
  ""
package/dist/machine.d.ts CHANGED
@@ -21,6 +21,7 @@ export declare const machine: {
21
21
  argv: string[];
22
22
  on?: string;
23
23
  }): Promise<number>;
24
+ daemonLine(): Promise<string>;
24
25
  logout(): Promise<void>;
25
26
  };
26
27
  export declare function tokenIn(line: string): string | undefined;
package/dist/machine.js CHANGED
@@ -37,7 +37,7 @@ export const machine = {
37
37
  await installService();
38
38
  probed = {
39
39
  at: Date.now(),
40
- tools: await setup.run({ install, rewire, say: (line) => process.stdout.write(`${line}\n`) }),
40
+ tools: await setup.run({ install, rewire, say: (text) => process.stdout.write(text) }),
41
41
  };
42
42
  return answer.machine;
43
43
  },
@@ -118,6 +118,17 @@ export const machine = {
118
118
  return runHere(argv);
119
119
  return runThere({ argv, on });
120
120
  },
121
+ async daemonLine() {
122
+ if (process.env.CRAFTSPACE_NO_SERVICE === '1')
123
+ return `not installed. Start it with: ${here()} daemon`;
124
+ if (process.platform !== 'linux')
125
+ return BEATING;
126
+ if (!(await serviceRunning()))
127
+ return `not running. Start it with: ${here()} daemon`;
128
+ if (!(await userLingers()))
129
+ return `beating, but it stops when you log out. Run: ${LINGER_LINE}`;
130
+ return BEATING;
131
+ },
121
132
  async logout() {
122
133
  const config = await readConfig();
123
134
  const token = await readToken();
@@ -348,8 +359,6 @@ async function readSessions() {
348
359
  }
349
360
  return sessions.slice(0, MAX_REPORTED_SESSIONS);
350
361
  }
351
- // Only the block between our markers is ours. Everything a person put in this file by hand is left exactly
352
- // where it was, because the fastest way to lock a team out of its own box is to rewrite this file wholesale.
353
362
  async function writeAuthorizedKeys(keys) {
354
363
  const target = path.join(os.homedir(), '.ssh', 'authorized_keys');
355
364
  const current = await readFile(target, 'utf8').catch(() => '');
@@ -387,6 +396,30 @@ async function writeAgentConfigs({ url, token, writes, }) {
387
396
  owned.push(target);
388
397
  }
389
398
  await writeFile(ownedPath(), JSON.stringify({ paths: owned }, null, 2));
399
+ await acceptClaudeGates(serversIn(planned));
400
+ }
401
+ function serversIn(writes) {
402
+ return writes.flatMap((write) => isPlainObject(write.contents.mcpServers) ? Object.keys(write.contents.mcpServers) : []);
403
+ }
404
+ async function acceptClaudeGates(servers) {
405
+ const target = path.join(os.homedir(), '.claude.json');
406
+ const raw = await readFile(target, 'utf8').catch(() => null);
407
+ if (raw === null)
408
+ return;
409
+ const parsed = JSON.parse(raw);
410
+ const projects = isPlainObject(parsed.projects) ? parsed.projects : {};
411
+ const found = projects[os.homedir()];
412
+ const here = isPlainObject(found) ? { ...found } : {};
413
+ const enabled = new Set([...(Array.isArray(here.enabledMcpjsonServers) ? here.enabledMcpjsonServers : []), ...servers]);
414
+ if (here.hasTrustDialogAccepted === true && enabled.size === asArray(here.enabledMcpjsonServers).length)
415
+ return;
416
+ here.hasTrustDialogAccepted = true;
417
+ here.enabledMcpjsonServers = [...enabled];
418
+ const next = { ...parsed, projects: { ...projects, [os.homedir()]: here } };
419
+ await writeFile(target, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
420
+ }
421
+ function asArray(value) {
422
+ return Array.isArray(value) ? value : [];
390
423
  }
391
424
  function mergeInto(current, incoming) {
392
425
  const merged = { ...current };
@@ -429,14 +462,13 @@ async function weCreated(target) {
429
462
  return backup === null;
430
463
  }
431
464
  async function installService() {
432
- if (process.env.CRAFTSPACE_NO_SERVICE === '1') {
433
- process.stdout.write('Skipping the service. Run the beat yourself with: craftspace daemon\n');
465
+ if (process.env.CRAFTSPACE_NO_SERVICE === '1')
434
466
  return;
435
- }
436
467
  if (process.platform === 'linux') {
437
468
  const dir = path.join(os.homedir(), '.config', 'systemd', 'user');
438
469
  await mkdir(dir, { recursive: true });
439
470
  await writeFile(path.join(dir, `${SERVICE_NAME}.service`), systemdUnit());
471
+ await run('loginctl', ['enable-linger', os.userInfo().username]).catch(() => undefined);
440
472
  await run('systemctl', ['--user', 'daemon-reload']).catch(() => undefined);
441
473
  await run('systemctl', ['--user', 'enable', '--now', SERVICE_NAME]).catch(() => undefined);
442
474
  return;
@@ -490,6 +522,16 @@ function launchdPlist() {
490
522
  </plist>
491
523
  `;
492
524
  }
525
+ function here() {
526
+ return `${process.execPath} ${entryPath()}`;
527
+ }
528
+ async function serviceRunning() {
529
+ return (await run('systemctl', ['--user', 'is-active', SERVICE_NAME]).catch(() => null)) !== null;
530
+ }
531
+ async function userLingers() {
532
+ const shown = await run('loginctl', ['show-user', os.userInfo().username, '-p', 'Linger']).catch(() => null);
533
+ return shown?.stdout.includes('Linger=yes') === true;
534
+ }
493
535
  function entryPath() {
494
536
  return path.join(path.dirname(new URL(import.meta.url).pathname), 'index.js');
495
537
  }
@@ -532,6 +574,8 @@ const MAX_BACKOFF_MS = 120_000;
532
574
  const KEYS_BEGIN = '# craftspace begin';
533
575
  const KEYS_END = '# craftspace end';
534
576
  const POLL_INTERVAL_MS = 1_000;
577
+ const BEATING = 'beating every 15s, outbound only, and keeps this set up';
578
+ const LINGER_LINE = `sudo loginctl enable-linger ${os.userInfo().username}`;
535
579
  const DEFAULT_URL = process.env.CRAFTSPACE_URL ?? 'https://craftspace.app';
536
580
  const PROBE_EVERY_MS = 300_000;
537
581
  let probed = null;
package/dist/run.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import os from 'node:os';
2
3
  import { MACHINE_RUN_OUTPUT_MAX, spreadIfDefined } from '@craftspace/shared';
3
4
  export const runner = {
4
5
  attended(argv) {
@@ -10,6 +11,7 @@ export const runner = {
10
11
  },
11
12
  async unattended({ work, onProgress, }) {
12
13
  const argv = headlessArgv(work);
14
+ const raw = argv[0] === 'script';
13
15
  let output = '';
14
16
  let agentSessionId = work.agentSessionId ?? undefined;
15
17
  let lastText = '';
@@ -33,10 +35,24 @@ export const runner = {
33
35
  }
34
36
  };
35
37
  const code = await new Promise((resolve) => {
36
- const child = spawn(argv[0], argv.slice(1), { stdio: ['ignore', 'pipe', 'pipe'] });
38
+ const child = spawn(argv[0], argv.slice(1), {
39
+ stdio: ['ignore', 'pipe', 'pipe'],
40
+ cwd: os.homedir(),
41
+ env: {
42
+ ...process.env,
43
+ TERM: process.env.TERM ?? 'xterm-256color',
44
+ COLUMNS: process.env.COLUMNS ?? '120',
45
+ LINES: process.env.LINES ?? '40',
46
+ },
47
+ });
48
+ append('');
37
49
  child.stdout.setEncoding('utf8');
38
50
  child.stderr.setEncoding('utf8');
39
51
  child.stdout.on('data', (chunk) => {
52
+ if (raw) {
53
+ append(chunk);
54
+ return;
55
+ }
40
56
  pending += chunk;
41
57
  const lines = pending.split('\n');
42
58
  pending = lines.pop() ?? '';
@@ -85,10 +101,13 @@ export function headlessArgv(work) {
85
101
  return [argv[0], '-p', argv.slice(1).join(' '), ...streaming];
86
102
  }
87
103
  function underTty(argv) {
104
+ const line = `stty rows ${PTY_ROWS} cols ${PTY_COLS} 2>/dev/null; exec ${argv.join(' ')}`;
88
105
  return process.platform === 'linux'
89
- ? ['script', '-qec', argv.join(' '), '/dev/null']
90
- : ['script', '-q', '/dev/null', ...argv];
106
+ ? ['script', '-qec', line, '/dev/null']
107
+ : ['script', '-q', '/dev/null', 'sh', '-c', line];
91
108
  }
109
+ const PTY_ROWS = 40;
110
+ const PTY_COLS = 120;
92
111
  function isAgent(command) {
93
112
  return AGENTS.has((command ?? '').split('/').pop() ?? '');
94
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@craftspace/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Sign a Mac or Linux machine into Craftspace and keep its agent setup in step.",
6
6
  "license": "MIT",