mira-cli-ts 0.26.6 → 0.26.8

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/cli.js +344 -1
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -74,8 +74,37 @@ function apiUrl() {
74
74
  return `http://127.0.0.1:${port}`;
75
75
  return DEFAULT_API.replace(/\/$/, "");
76
76
  }
77
+ function readMiraEnvFile() {
78
+ try {
79
+ const { readFileSync, existsSync } = __require("fs");
80
+ const { homedir } = __require("os");
81
+ const { join } = __require("path");
82
+ const cands = [
83
+ process.env.MIRA_DIR?.trim() ? join(process.env.MIRA_DIR.trim(), "mira.env") : null,
84
+ process.env.XDG_CONFIG_HOME?.trim() ? join(process.env.XDG_CONFIG_HOME.trim(), "mira", "mira.env") : null,
85
+ join(homedir(), ".mira", "mira.env")
86
+ ].filter(Boolean);
87
+ for (const p of cands) {
88
+ try {
89
+ if (!existsSync(p))
90
+ continue;
91
+ for (const line of readFileSync(p, "utf-8").split(`
92
+ `)) {
93
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
94
+ const k = m?.[1];
95
+ if (k === "MIRA_TOKEN") {
96
+ const v = (m?.[2] ?? "").replace(/^(['"])(.*)\1$/, "$2").trim();
97
+ if (v)
98
+ return v;
99
+ }
100
+ }
101
+ } catch {}
102
+ }
103
+ } catch {}
104
+ return;
105
+ }
77
106
  function token() {
78
- return process.env.MIRA_TOKEN ?? "";
107
+ return process.env.MIRA_TOKEN ?? readMiraEnvFile() ?? "";
79
108
  }
80
109
  function authHeaders() {
81
110
  const t = token();
@@ -136,6 +165,11 @@ Usage:
136
165
  mira config get [key] Get config (or filtered by key)
137
166
  mira config set <key> <value> Set config (dot notation, JSON value)
138
167
  mira finding list [--status open] [--limit 20] List findings
168
+ mira workspace list List workspaces (recent)
169
+ mira workspace add <path> Add workspace (validates path)
170
+ mira workspace remove <id|path> Remove workspace
171
+ mira workspace switch <id|path> Switch workspace (sets cwd for new sessions)
172
+ mira project init [--template ts] [--path <dir>] Init mira.json in project
139
173
  mira manager Active jobs + recent sessions
140
174
  mira health Liveness (/healthz)
141
175
  mira complete --prefix "..." [--suffix "..."] [--file path] Ghost-text completion
@@ -145,11 +179,15 @@ Usage:
145
179
  Env:
146
180
  MIRA_API_URL server URL (default http://127.0.0.1:4096)
147
181
  MIRA_TOKEN bearer token
182
+ MIRA_WORKSPACE single workspace path (env)
183
+ MIRA_WORKSPACE_ROOTS comma-separated workspace roots (env)
148
184
 
149
185
  Examples:
150
186
  mira serve
151
187
  mira session create --agent ask --title "Q&A"
152
188
  mira session prompt --id abc --prompt "explain ./src/index.ts"
189
+ mira workspace add /path/to/repo
190
+ mira project init --template ts
153
191
  mira skill list
154
192
  mira command list
155
193
  mira tool list
@@ -501,6 +539,282 @@ async function cmdHealth() {
501
539
  }
502
540
  console.log(JSON.stringify(await res.json(), null, 2));
503
541
  }
542
+ async function cmdWorkspaceList() {
543
+ try {
544
+ const res = await apiFetch("/workspaces");
545
+ if (!res.ok) {
546
+ console.error(`workspace list failed: ${res.status} ${await res.text()}`);
547
+ process.exit(1);
548
+ }
549
+ const data = await res.json();
550
+ const list = data.workspaces ?? [];
551
+ if (list.length === 0) {
552
+ console.log("No workspaces \u2014 add one with: mira workspace add /path/to/repo");
553
+ return;
554
+ }
555
+ for (const w of list) {
556
+ console.log(`${w.id.slice(0, 8)} ${w.path} (${w.name})`);
557
+ }
558
+ } catch (e) {
559
+ try {
560
+ const { readFileSync, existsSync } = __require("fs");
561
+ const home = process.env.HOME ?? "";
562
+ const fp = home ? `${home}/.mira/workspaces.json` : `${process.cwd()}/.mira/workspaces.json`;
563
+ if (existsSync(fp)) {
564
+ const raw = readFileSync(fp, "utf-8");
565
+ const parsed = JSON.parse(raw);
566
+ const list = Array.isArray(parsed) ? parsed : parsed.workspaces ?? [];
567
+ if (list.length === 0)
568
+ console.log("No workspaces");
569
+ else
570
+ for (const w of list)
571
+ console.log(`${(w.id ?? "").slice(0, 8)} ${w.path} (${w.name ?? w.path.split("/").pop()})`);
572
+ return;
573
+ }
574
+ } catch {}
575
+ console.error(`workspace list failed: ${String(e.message ?? e)}`);
576
+ process.exit(1);
577
+ }
578
+ }
579
+ async function cmdWorkspaceAdd(opts) {
580
+ const positional = Bun.argv.slice(3).filter((a) => !a.startsWith("-"));
581
+ let rawPath = String(opts.path ?? opts.p ?? opts["1"] ?? positional[1] ?? "").trim();
582
+ if (!rawPath || rawPath === "add")
583
+ rawPath = String(positional[1] ?? opts["1"] ?? "").trim();
584
+ if (!rawPath)
585
+ rawPath = String(positional[0] ?? "").trim();
586
+ if (!rawPath || rawPath === "add") {
587
+ console.error("workspace add requires <path> \u2014 e.g. mira workspace add /path/to/repo");
588
+ process.exit(1);
589
+ }
590
+ try {
591
+ const res = await apiFetch("/workspaces", { method: "POST", body: JSON.stringify({ path: rawPath }) });
592
+ if (!res.ok) {
593
+ console.error(`workspace add failed: ${res.status} ${await res.text()}`);
594
+ process.exit(1);
595
+ }
596
+ const data = await res.json();
597
+ console.log(`Added workspace: ${data.workspace.path} (${data.workspace.id.slice(0, 8)})`);
598
+ console.log(JSON.stringify(data.workspace, null, 2));
599
+ } catch (e) {
600
+ const msg = String(e.message ?? e);
601
+ const isConn = msg.includes("ECONNREFUSED") || msg.includes("fetch failed") || msg.includes("Connection refused") || msg.includes("Unable to connect") || msg.includes("ECONNRESET");
602
+ if (!isConn) {
603
+ console.error(`workspace add failed: ${msg}`);
604
+ process.exit(1);
605
+ }
606
+ try {
607
+ const { readFileSync, existsSync, mkdirSync, writeFileSync } = __require("fs");
608
+ const { resolve } = __require("path");
609
+ const absPath = rawPath.startsWith("/") ? rawPath : resolve(process.cwd(), rawPath);
610
+ if (!existsSync(absPath)) {
611
+ console.error(`path not found: ${absPath}`);
612
+ process.exit(1);
613
+ }
614
+ const home = process.env.HOME ?? "";
615
+ const fp = home ? `${home}/.mira/workspaces.json` : `${process.cwd()}/.mira/workspaces.json`;
616
+ const dir = fp.slice(0, fp.lastIndexOf("/"));
617
+ if (dir)
618
+ mkdirSync(dir, { recursive: true });
619
+ let existing = [];
620
+ if (existsSync(fp)) {
621
+ try {
622
+ const raw = readFileSync(fp, "utf-8");
623
+ const parsed = JSON.parse(raw);
624
+ existing = Array.isArray(parsed) ? parsed : parsed.workspaces ?? [];
625
+ } catch {}
626
+ }
627
+ if (existing.some((w) => w.path === absPath)) {
628
+ console.log(`Workspace already exists: ${absPath}`);
629
+ return;
630
+ }
631
+ const id = Buffer.from(absPath).toString("base64url");
632
+ const entry = { id, path: absPath, name: absPath.split("/").pop() || absPath, addedAt: Date.now() };
633
+ existing.push(entry);
634
+ writeFileSync(fp, JSON.stringify({ workspaces: existing }, null, 2) + `
635
+ `);
636
+ console.log(`Added workspace (offline): ${absPath} (${id.slice(0, 8)})`);
637
+ } catch (err) {
638
+ console.error(`workspace add failed: ${String(err.message ?? err)}`);
639
+ process.exit(1);
640
+ }
641
+ }
642
+ }
643
+ async function cmdWorkspaceRemove(opts) {
644
+ const positional = Bun.argv.slice(3).filter((a) => !a.startsWith("-"));
645
+ let id = String(opts.id ?? opts["1"] ?? positional[1] ?? "").trim();
646
+ if (!id || id === "remove" || id === "rm" || id === "delete")
647
+ id = String(positional[1] ?? opts["1"] ?? "").trim();
648
+ if (!id)
649
+ id = String(positional[0] ?? "").trim();
650
+ if (!id || ["remove", "rm", "delete"].includes(id)) {
651
+ console.error("workspace remove requires <id|path> \u2014 e.g. mira workspace remove <id>");
652
+ process.exit(1);
653
+ }
654
+ try {
655
+ const res = await apiFetch(`/workspaces/${encodeURIComponent(id)}`, { method: "DELETE" });
656
+ if (!res.ok) {
657
+ console.error(`workspace remove failed: ${res.status} ${await res.text()}`);
658
+ process.exit(1);
659
+ }
660
+ console.log(JSON.stringify(await res.json(), null, 2));
661
+ return;
662
+ } catch (e) {
663
+ const msg = String(e.message ?? e);
664
+ const isConn = msg.includes("ECONNREFUSED") || msg.includes("fetch failed") || msg.includes("Connection refused") || msg.includes("Unable to connect") || msg.includes("ECONNRESET");
665
+ if (!isConn) {
666
+ console.error(`workspace remove failed: ${msg}`);
667
+ process.exit(1);
668
+ }
669
+ try {
670
+ const { readFileSync, existsSync, writeFileSync } = __require("fs");
671
+ const home = process.env.HOME ?? "";
672
+ const fp = home ? `${home}/.mira/workspaces.json` : `${process.cwd()}/.mira/workspaces.json`;
673
+ if (!existsSync(fp)) {
674
+ console.error(`workspace not found: ${id}`);
675
+ process.exit(1);
676
+ }
677
+ const raw = readFileSync(fp, "utf-8");
678
+ const parsed = JSON.parse(raw);
679
+ const list = Array.isArray(parsed) ? parsed : parsed.workspaces ?? [];
680
+ const idx = list.findIndex((w) => w.id === id || w.path === id || w.id.startsWith(id) || w.path.endsWith(id));
681
+ if (idx === -1) {
682
+ console.error(`workspace not found: ${id}`);
683
+ process.exit(1);
684
+ }
685
+ const removed = list[idx];
686
+ const next = list.filter((_, i) => i !== idx);
687
+ writeFileSync(fp, JSON.stringify({ workspaces: next }, null, 2) + `
688
+ `);
689
+ console.log(`Removed workspace (offline): ${removed.path}`);
690
+ console.log(JSON.stringify({ ok: true, removed }, null, 2));
691
+ } catch (err) {
692
+ console.error(`workspace remove failed: ${String(err.message ?? err)}`);
693
+ process.exit(1);
694
+ }
695
+ }
696
+ }
697
+ async function cmdWorkspaceSwitch(opts) {
698
+ const positional = Bun.argv.slice(3).filter((a) => !a.startsWith("-"));
699
+ let target = String(opts.path ?? opts["1"] ?? positional[1] ?? "").trim();
700
+ if (!target || target === "switch")
701
+ target = String(positional[1] ?? opts["1"] ?? "").trim();
702
+ if (!target)
703
+ target = String(positional[0] ?? "").trim();
704
+ try {
705
+ const res = await apiFetch("/workspaces");
706
+ if (res.ok) {
707
+ const data = await res.json();
708
+ const found = data.workspaces.find((w) => w.id === target || w.path === target || w.id.startsWith(target) || w.path.endsWith(target));
709
+ if (found) {
710
+ console.log(`Switched to workspace: ${found.path} (${found.name})`);
711
+ console.log(`Set MIRA_WORKSPACE=${found.path} for new sessions (export MIRA_WORKSPACE="${found.path}")`);
712
+ return;
713
+ }
714
+ console.error(`workspace not found: ${target}`);
715
+ process.exit(1);
716
+ }
717
+ } catch {}
718
+ try {
719
+ const { readFileSync, existsSync } = __require("fs");
720
+ const home = process.env.HOME ?? "";
721
+ const fp = home ? `${home}/.mira/workspaces.json` : `${process.cwd()}/.mira/workspaces.json`;
722
+ if (existsSync(fp)) {
723
+ const raw = readFileSync(fp, "utf-8");
724
+ const parsed = JSON.parse(raw);
725
+ const list = Array.isArray(parsed) ? parsed : parsed.workspaces ?? [];
726
+ const found = list.find((w) => w.id === target || w.path === target || w.id.startsWith(target));
727
+ if (found) {
728
+ console.log(`Switched to workspace: ${found.path} (${found.name ?? found.path.split("/").pop()})`);
729
+ console.log(`Set MIRA_WORKSPACE=${found.path} for new sessions (export MIRA_WORKSPACE="${found.path}")`);
730
+ return;
731
+ }
732
+ }
733
+ } catch {}
734
+ console.error(`workspace not found: ${target}`);
735
+ process.exit(1);
736
+ }
737
+ async function cmdProjectInit(opts) {
738
+ const template = String(opts.template ?? opts.t ?? "default").trim() || "default";
739
+ const targetPath = String(opts.path ?? opts.p ?? positionalPath() ?? process.cwd()).trim() || process.cwd();
740
+ function positionalPath() {
741
+ const positional = Bun.argv.slice(3).filter((a) => !a.startsWith("-"));
742
+ const tIdx = Bun.argv.indexOf("--template");
743
+ const tIdx2 = Bun.argv.indexOf("-t");
744
+ let afterTemplate = -1;
745
+ if (tIdx !== -1)
746
+ afterTemplate = tIdx + 2;
747
+ else if (tIdx2 !== -1)
748
+ afterTemplate = tIdx2 + 2;
749
+ if (afterTemplate !== -1 && Bun.argv[afterTemplate] && !Bun.argv[afterTemplate].startsWith("-"))
750
+ return Bun.argv[afterTemplate];
751
+ if (template !== "default" && positional.length > 0) {
752
+ if (positional[0] === template)
753
+ return positional[1];
754
+ }
755
+ return positional[0];
756
+ }
757
+ const { existsSync, mkdirSync, writeFileSync, readFileSync } = __require("fs");
758
+ const { resolve } = __require("path");
759
+ const absPath = targetPath.startsWith("/") ? targetPath : resolve(process.cwd(), targetPath);
760
+ try {
761
+ mkdirSync(absPath, { recursive: true });
762
+ } catch {}
763
+ const miraJsonPath = `${absPath}/mira.json`;
764
+ if (existsSync(miraJsonPath)) {
765
+ console.error(`mira.json already exists at ${miraJsonPath} \u2014 refusing to overwrite`);
766
+ process.exit(1);
767
+ }
768
+ let config;
769
+ if (template === "ts" || template === "typescript") {
770
+ config = {
771
+ model: "openrouter/anthropic/claude-sonnet-4",
772
+ permission: { bash: "ask", read: "allow", write: "ask", edit: "ask" },
773
+ guardrails: { allowedRoots: ["."], enforce: false },
774
+ mcp: {},
775
+ provider: {},
776
+ agents: {}
777
+ };
778
+ } else {
779
+ config = {
780
+ model: "openrouter/anthropic/claude-sonnet-4",
781
+ permission: {},
782
+ mcp: {},
783
+ provider: {}
784
+ };
785
+ }
786
+ writeFileSync(miraJsonPath, JSON.stringify(config, null, 2) + `
787
+ `);
788
+ console.log(`Created ${miraJsonPath} (template: ${template})`);
789
+ try {
790
+ const res = await apiFetch("/workspaces", { method: "POST", body: JSON.stringify({ path: absPath }) });
791
+ if (res.ok) {
792
+ const data = await res.json();
793
+ console.log(`Registered workspace: ${data.workspace.path}`);
794
+ }
795
+ } catch {}
796
+ try {
797
+ const home = process.env.HOME ?? "";
798
+ const fp = home ? `${home}/.mira/workspaces.json` : `${process.cwd()}/.mira/workspaces.json`;
799
+ const dir = fp.slice(0, fp.lastIndexOf("/"));
800
+ if (dir)
801
+ mkdirSync(dir, { recursive: true });
802
+ let existing = [];
803
+ if (existsSync(fp)) {
804
+ try {
805
+ const raw = readFileSync(fp, "utf-8");
806
+ const parsed = JSON.parse(raw);
807
+ existing = Array.isArray(parsed) ? parsed : parsed.workspaces ?? [];
808
+ } catch {}
809
+ }
810
+ if (!existing.some((w) => w.path === absPath)) {
811
+ const id = Buffer.from(absPath).toString("base64url");
812
+ existing.push({ id, path: absPath, name: absPath.split("/").pop() || absPath, addedAt: Date.now() });
813
+ writeFileSync(fp, JSON.stringify({ workspaces: existing }, null, 2) + `
814
+ `);
815
+ }
816
+ } catch {}
817
+ }
504
818
  async function main() {
505
819
  const rawCmd = Bun.argv[2] ?? "help";
506
820
  if (rawCmd.startsWith("/")) {
@@ -610,6 +924,35 @@ async function main() {
610
924
  process.exit(1);
611
925
  }
612
926
  return;
927
+ case "workspace":
928
+ case "workspaces":
929
+ if (sub === "list" || sub === null)
930
+ await cmdWorkspaceList();
931
+ else if (sub === "add")
932
+ await cmdWorkspaceAdd(opts);
933
+ else if (sub === "remove" || sub === "rm" || sub === "delete")
934
+ await cmdWorkspaceRemove(opts);
935
+ else if (sub === "switch")
936
+ await cmdWorkspaceSwitch(opts);
937
+ else {
938
+ if (sub && !["list", "add", "remove", "rm", "delete", "switch"].includes(sub)) {
939
+ opts["path"] = sub;
940
+ await cmdWorkspaceAdd(opts);
941
+ return;
942
+ }
943
+ console.error(`unknown workspace subcommand: ${sub ?? ""} \u2014 try: list, add, remove, switch`);
944
+ process.exit(1);
945
+ }
946
+ return;
947
+ case "project":
948
+ case "projects":
949
+ if (sub === "init" || sub === null)
950
+ await cmdProjectInit(opts);
951
+ else {
952
+ console.error(`unknown project subcommand: ${sub ?? ""} \u2014 try: init`);
953
+ process.exit(1);
954
+ }
955
+ return;
613
956
  case "complete":
614
957
  case "autocomplete":
615
958
  await cmdComplete(opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mira-cli-ts",
3
- "version": "0.26.6",
3
+ "version": "0.26.8",
4
4
  "description": "Mira CLI — thin wrapper around @mira/server (serve + session/agent/complete)",
5
5
  "type": "module",
6
6
  "bin": {