stackmem 1.0.3 → 1.0.4

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 (4) hide show
  1. package/README.md +7 -1
  2. package/dist/cli.js +205 -46
  3. package/package.json +1 -1
  4. package/src/cli.ts +225 -46
package/README.md CHANGED
@@ -16,7 +16,13 @@ Then in any git repo:
16
16
  cm init <project> .
17
17
  ```
18
18
 
19
- That's it. No config, no accounts, no env file.
19
+ `cm init <project> .` does three things automatically:
20
+
21
+ 1. Installs a git post-commit hook
22
+ 2. Registers the stackmem MCP server with Claude Code
23
+ 3. Writes a CLAUDE.md to the project
24
+
25
+ After that, memories load automatically when you open Claude Code and save automatically on every commit. No further setup needed.
20
26
 
21
27
  ## Usage
22
28
 
package/dist/cli.js CHANGED
@@ -25,63 +25,68 @@ const TYPE_LABELS = {
25
25
  };
26
26
  function usage() {
27
27
  console.error(`Usage:
28
- cm start <project>
29
- cm save <project> <type> <content> [--force] (type: decision|rejection|constraint|discovery)
30
- cm fix <project> <problem> <solution>
31
- cm analyze <project> <path>
32
- cm search <project> <query>
33
- cm resolve <project> <memory-id>
34
- cm delete <project> <memory-id>
35
- cm compress <project>
28
+ cm start [project]
29
+ cm save [project] <type> <content> [--force] (type: decision|rejection|constraint|discovery)
30
+ cm fix [project] <problem> <solution>
31
+ cm analyze [project] <path>
32
+ cm search [project] <query>
33
+ cm resolve [project] <memory-id>
34
+ cm delete [project] <memory-id>
35
+ cm compress [project]
36
36
  cm init <project> <path>
37
- cm context <project> <task>
38
- cm help`);
37
+ cm context [project] <task>
38
+ cm help
39
+
40
+ [project] is optional if a .stackmem file exists in the current
41
+ directory (written by 'cm init'). Otherwise it must be given explicitly.`);
39
42
  process.exit(1);
40
43
  }
41
44
  function cmdHelp() {
42
45
  console.log(`cm — coding memory CLI
43
46
 
44
- cm start <project>
47
+ [project] is optional everywhere below if a .stackmem file exists in
48
+ the current directory (written by 'cm init'). Otherwise pass it explicitly.
49
+
50
+ cm start [project]
45
51
  Load all unresolved memories for a project, sorted by decay
46
52
  score and capped at a 2000 token context budget, grouped by
47
53
  type (decisions, constraints, discoveries, rejections), with
48
54
  linked memory ids shown under each entry.
49
55
 
50
- cm save <project> <type> <content> [--force]
56
+ cm save [project] <type> <content> [--force]
51
57
  Save a new memory. <type> is one of: decision, rejection,
52
58
  constraint, discovery. Automatically links to related existing
53
59
  memories. If the new memory contradicts an existing one, prompts
54
60
  to keep the old memory or replace it — pass --force to skip the
55
61
  prompt and always replace.
56
62
 
57
- cm fix <project> <problem> <solution>
63
+ cm fix [project] <problem> <solution>
58
64
  Record a problem and its solution for future reference.
59
65
 
60
- cm analyze <project> <path>
66
+ cm analyze [project] <path>
61
67
  Build an AST index (files, functions, classes) for the codebase
62
68
  at <path>.
63
69
 
64
- cm search <project> <query>
70
+ cm search [project] <query>
65
71
  Search saved memories and past fixes for a project matching
66
72
  <query>.
67
73
 
68
- cm resolve <project> <memory-id>
74
+ cm resolve [project] <memory-id>
69
75
  Mark a memory as resolved.
70
76
 
71
- cm delete <project> <memory-id>
77
+ cm delete [project] <memory-id>
72
78
  Delete a memory and any memory_links referencing it.
73
79
 
74
- cm compress <project>
80
+ cm compress [project]
75
81
  Cluster related unresolved memories (needs 20+) and collapse
76
82
  clusters of 3 or more into a single summary memory.
77
83
 
78
84
  cm init <project> <path>
79
- Install a git post-commit hook in the repo at <path> that
80
- records the commit message and any implicit memories (removed
81
- imports, new env vars, new auth-related files) after every
82
- commit.
85
+ Install a git post-commit hook in the repo at <path>, register
86
+ the stackmem MCP server with Claude Code, write a CLAUDE.md and
87
+ .stackmem file, and seed initial memories from the project scan.
83
88
 
84
- cm context <project> <task>
89
+ cm context [project] <task>
85
90
  Return only the top 5 memories most relevant to a specific
86
91
  task, ranked by a blend of task relevance and decay score,
87
92
  as a markdown block capped at a 2000 token budget.
@@ -93,6 +98,45 @@ function fail(message) {
93
98
  console.error(`Error: ${message}`);
94
99
  process.exit(1);
95
100
  }
101
+ // --- optional project resolution ------------------------------------------
102
+ const STACKMEM_FILE = ".stackmem";
103
+ function readProjectFromFile() {
104
+ const stackmemPath = nodePath.join(process.cwd(), STACKMEM_FILE);
105
+ if (!nodeFs.existsSync(stackmemPath))
106
+ return null;
107
+ const content = nodeFs.readFileSync(stackmemPath, "utf-8").trim();
108
+ return content.length > 0 ? content : null;
109
+ }
110
+ function requireProject(explicit) {
111
+ if (explicit)
112
+ return explicit;
113
+ const fromFile = readProjectFromFile();
114
+ if (fromFile)
115
+ return fromFile;
116
+ console.error("No project specified and no .stackmem file found. Run 'cm init <project> .' first or pass a project name.");
117
+ process.exit(1);
118
+ }
119
+ // For commands whose non-project args have a fixed count: if exactly that
120
+ // many args are given, project was omitted; if one more, the first is the
121
+ // project. Anything else is a usage error.
122
+ function splitOptionalProject(args, fixedArgCount) {
123
+ if (args.length === fixedArgCount)
124
+ return { project: null, rest: args };
125
+ if (args.length === fixedArgCount + 1)
126
+ return { project: args[0], rest: args.slice(1) };
127
+ return null;
128
+ }
129
+ // For commands whose remaining arg is free-form text (a query or task),
130
+ // arg count can't disambiguate an omitted project from a multi-word query —
131
+ // "cm search bug fix" is ambiguous by count alone. Use .stackmem's presence
132
+ // instead: if it exists, nothing is positionally a project.
133
+ function splitProjectFromVariadic(args) {
134
+ if (readProjectFromFile() !== null) {
135
+ return { project: null, rest: args };
136
+ }
137
+ const [project = null, ...rest] = args;
138
+ return { project, rest };
139
+ }
96
140
  // --- cm start ----------------------------------------------------------
97
141
  async function cmdStart(project) {
98
142
  const { data, error } = await supabase
@@ -389,11 +433,20 @@ async function cmdDelete(project, memoryId) {
389
433
  // .git/hooks/post-commit) so its module type is unambiguous — an
390
434
  // extensionless file run by git would otherwise have its CommonJS/ESM
391
435
  // interpretation depend on the target repo's own package.json.
392
- function buildHookLogicScript(project, cliPath) {
436
+ function buildHookLogicScript(cliPath) {
393
437
  return `#!/usr/bin/env node
394
438
  import { execFileSync } from "node:child_process";
439
+ import { readFileSync } from "node:fs";
440
+ import { join } from "node:path";
441
+
442
+ const stackmemPath = join(process.cwd(), ".stackmem");
443
+ let PROJECT;
444
+ try {
445
+ PROJECT = readFileSync(stackmemPath, "utf8").trim();
446
+ } catch {
447
+ process.exit(0); // no .stackmem, skip silently
448
+ }
395
449
 
396
- const PROJECT = ${JSON.stringify(project)};
397
450
  const CLI_PATH = ${JSON.stringify(cliPath)};
398
451
 
399
452
  function git(args) {
@@ -611,6 +664,69 @@ cm save ${project} <type> "<content>"
611
664
  nodeFs.writeFileSync(claudeMdPath, content, "utf-8");
612
665
  console.log("CLAUDE.md written.");
613
666
  }
667
+ function writeProjectFile(project, resolvedPath) {
668
+ const stackmemPath = nodePath.join(resolvedPath, STACKMEM_FILE);
669
+ nodeFs.writeFileSync(stackmemPath, `${project}\n`, "utf-8");
670
+ }
671
+ // Scans the freshly-initialized project for a few cheap, high-signal facts
672
+ // and saves them as memories directly (bypassing cmdSave's duplicate/conflict
673
+ // prompt — there's nothing to conflict with yet, and init must stay
674
+ // non-interactive). Returns how many memories were actually saved.
675
+ async function seedMemories(project, resolvedPath) {
676
+ let seededCount = 0;
677
+ const packageJsonPath = nodePath.join(resolvedPath, "package.json");
678
+ if (nodeFs.existsSync(packageJsonPath)) {
679
+ try {
680
+ const pkg = JSON.parse(nodeFs.readFileSync(packageJsonPath, "utf-8"));
681
+ const depNames = [
682
+ ...Object.keys(pkg.dependencies ?? {}),
683
+ ...Object.keys(pkg.devDependencies ?? {}),
684
+ ];
685
+ if (depNames.length > 0) {
686
+ const { error } = await supabase.from("memories").insert({
687
+ project,
688
+ type: "discovery",
689
+ content: `tech stack: ${depNames.join(", ")}`,
690
+ device_id: getDeviceId(),
691
+ });
692
+ if (!error)
693
+ seededCount++;
694
+ }
695
+ }
696
+ catch {
697
+ // Malformed package.json — skip the tech-stack memory.
698
+ }
699
+ }
700
+ const envExamplePath = nodePath.join(resolvedPath, ".env.example");
701
+ if (nodeFs.existsSync(envExamplePath)) {
702
+ const varNames = nodeFs
703
+ .readFileSync(envExamplePath, "utf-8")
704
+ .split("\n")
705
+ .filter((line) => /^[A-Z_]+=/.test(line))
706
+ .map((line) => line.split("=")[0]);
707
+ if (varNames.length > 0) {
708
+ const { error } = await supabase.from("memories").insert({
709
+ project,
710
+ type: "constraint",
711
+ content: `required env vars: ${varNames.join(", ")}`,
712
+ device_id: getDeviceId(),
713
+ });
714
+ if (!error)
715
+ seededCount++;
716
+ }
717
+ }
718
+ analyzeCodebase(resolvedPath);
719
+ return seededCount;
720
+ }
721
+ // Smoke-tests the same anon-key + device-header path session_start/cmdStart
722
+ // use, without pulling in all of cmdStart's scoring/formatting logic.
723
+ async function verifyBackendConnection(project) {
724
+ const { error } = await supabase
725
+ .from("memories")
726
+ .select("id", { count: "exact", head: true })
727
+ .eq("project", project);
728
+ return !error;
729
+ }
614
730
  async function cmdInit(project, targetPath) {
615
731
  const resolvedPath = nodePath.resolve(targetPath);
616
732
  const gitDir = nodePath.join(resolvedPath, ".git");
@@ -621,7 +737,7 @@ async function cmdInit(project, targetPath) {
621
737
  const codingMemoryDir = nodePath.join(resolvedPath, ".coding-memory");
622
738
  nodeFs.mkdirSync(codingMemoryDir, { recursive: true });
623
739
  const hookLogicPath = nodePath.join(codingMemoryDir, "post-commit-hook.mjs");
624
- nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(project, DIST_CLI_PATH), "utf-8");
740
+ nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(DIST_CLI_PATH), "utf-8");
625
741
  const hooksDir = nodePath.join(gitDir, "hooks");
626
742
  nodeFs.mkdirSync(hooksDir, { recursive: true });
627
743
  const hookPath = nodePath.join(hooksDir, "post-commit");
@@ -630,6 +746,26 @@ async function cmdInit(project, targetPath) {
630
746
  console.log(`coding-memory hook installed for project ${project} at ${targetPath}`);
631
747
  registerMcpServer(DIST_INDEX_PATH);
632
748
  writeClaudeMd(project, resolvedPath);
749
+ const seededCount = await seedMemories(project, resolvedPath);
750
+ console.log(`Seeded ${seededCount} initial memories from project scan.`);
751
+ const backendOk = await verifyBackendConnection(project);
752
+ console.log("");
753
+ console.log("✓ Git hook installed");
754
+ console.log("✓ MCP server registered");
755
+ console.log("✓ CLAUDE.md written");
756
+ console.log(`✓ Seeded ${seededCount} memories from project scan`);
757
+ if (backendOk) {
758
+ console.log("✓ Backend connection verified");
759
+ console.log("");
760
+ console.log(`stackmem is live. Open Claude Code and ask "what do you know about this project?" to verify.`);
761
+ }
762
+ else {
763
+ console.log("✗ Backend connection failed — run 'cm doctor' (once we build it)");
764
+ }
765
+ // Written last and deliberately: if a commit (and hence the post-commit
766
+ // hook) fires anywhere during init, it must still see whatever project
767
+ // .stackmem pointed to before this run, not the one being initialized now.
768
+ writeProjectFile(project, resolvedPath);
633
769
  }
634
770
  // --- cm context ------------------------------------------------------------
635
771
  const CONTEXT_TYPE_ORDER = ["decision", "constraint", "rejection", "discovery"];
@@ -679,9 +815,10 @@ async function main() {
679
815
  const [, , command, ...args] = process.argv;
680
816
  switch (command) {
681
817
  case "start": {
682
- const [project] = args;
683
- if (!project)
818
+ const split = splitOptionalProject(args, 0);
819
+ if (!split)
684
820
  usage();
821
+ const project = requireProject(split.project);
685
822
  await cmdStart(project);
686
823
  break;
687
824
  }
@@ -691,51 +828,72 @@ async function main() {
691
828
  const positional = force
692
829
  ? [...args.slice(0, forceIndex), ...args.slice(forceIndex + 1)]
693
830
  : args;
694
- const [project, type, ...rest] = positional;
695
- if (!project || !type || rest.length === 0)
831
+ let explicitProject;
832
+ let type;
833
+ let rest;
834
+ if (MEMORY_TYPES.includes(positional[0])) {
835
+ explicitProject = null;
836
+ [type, ...rest] = positional;
837
+ }
838
+ else {
839
+ explicitProject = positional[0] ?? null;
840
+ [, type, ...rest] = positional;
841
+ }
842
+ if (!type || rest.length === 0)
696
843
  usage();
844
+ const project = requireProject(explicitProject);
697
845
  await cmdSave(project, type, rest.join(" "), force);
698
846
  break;
699
847
  }
700
848
  case "fix": {
701
- const [project, problem, solution] = args;
702
- if (!project || !problem || !solution)
849
+ const split = splitOptionalProject(args, 2);
850
+ if (!split)
703
851
  usage();
852
+ const project = requireProject(split.project);
853
+ const [problem, solution] = split.rest;
704
854
  await cmdFix(project, problem, solution);
705
855
  break;
706
856
  }
707
857
  case "analyze": {
708
- const [project, path] = args;
709
- if (!project || !path)
858
+ const split = splitOptionalProject(args, 1);
859
+ if (!split)
710
860
  usage();
861
+ const project = requireProject(split.project);
862
+ const [path] = split.rest;
711
863
  await cmdAnalyze(project, path);
712
864
  break;
713
865
  }
714
866
  case "search": {
715
- const [project, ...rest] = args;
716
- if (!project || rest.length === 0)
867
+ const split = splitProjectFromVariadic(args);
868
+ const project = requireProject(split.project);
869
+ if (split.rest.length === 0)
717
870
  usage();
718
- await cmdSearch(project, rest.join(" "));
871
+ await cmdSearch(project, split.rest.join(" "));
719
872
  break;
720
873
  }
721
874
  case "resolve": {
722
- const [project, memoryId] = args;
723
- if (!project || !memoryId)
875
+ const split = splitOptionalProject(args, 1);
876
+ if (!split)
724
877
  usage();
878
+ const project = requireProject(split.project);
879
+ const [memoryId] = split.rest;
725
880
  await cmdResolve(project, memoryId);
726
881
  break;
727
882
  }
728
883
  case "delete": {
729
- const [project, memoryId] = args;
730
- if (!project || !memoryId)
884
+ const split = splitOptionalProject(args, 1);
885
+ if (!split)
731
886
  usage();
887
+ const project = requireProject(split.project);
888
+ const [memoryId] = split.rest;
732
889
  await cmdDelete(project, memoryId);
733
890
  break;
734
891
  }
735
892
  case "compress": {
736
- const [project] = args;
737
- if (!project)
893
+ const split = splitOptionalProject(args, 0);
894
+ if (!split)
738
895
  usage();
896
+ const project = requireProject(split.project);
739
897
  await cmdCompress(project);
740
898
  break;
741
899
  }
@@ -747,10 +905,11 @@ async function main() {
747
905
  break;
748
906
  }
749
907
  case "context": {
750
- const [project, ...rest] = args;
751
- if (!project || rest.length === 0)
908
+ const split = splitProjectFromVariadic(args);
909
+ const project = requireProject(split.project);
910
+ if (split.rest.length === 0)
752
911
  usage();
753
- await cmdContext(project, rest.join(" "));
912
+ await cmdContext(project, split.rest.join(" "));
754
913
  break;
755
914
  }
756
915
  case "help": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stackmem",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -39,64 +39,69 @@ const TYPE_LABELS: Record<MemoryType, string> = {
39
39
 
40
40
  function usage(): never {
41
41
  console.error(`Usage:
42
- cm start <project>
43
- cm save <project> <type> <content> [--force] (type: decision|rejection|constraint|discovery)
44
- cm fix <project> <problem> <solution>
45
- cm analyze <project> <path>
46
- cm search <project> <query>
47
- cm resolve <project> <memory-id>
48
- cm delete <project> <memory-id>
49
- cm compress <project>
42
+ cm start [project]
43
+ cm save [project] <type> <content> [--force] (type: decision|rejection|constraint|discovery)
44
+ cm fix [project] <problem> <solution>
45
+ cm analyze [project] <path>
46
+ cm search [project] <query>
47
+ cm resolve [project] <memory-id>
48
+ cm delete [project] <memory-id>
49
+ cm compress [project]
50
50
  cm init <project> <path>
51
- cm context <project> <task>
52
- cm help`);
51
+ cm context [project] <task>
52
+ cm help
53
+
54
+ [project] is optional if a .stackmem file exists in the current
55
+ directory (written by 'cm init'). Otherwise it must be given explicitly.`);
53
56
  process.exit(1);
54
57
  }
55
58
 
56
59
  function cmdHelp() {
57
60
  console.log(`cm — coding memory CLI
58
61
 
59
- cm start <project>
62
+ [project] is optional everywhere below if a .stackmem file exists in
63
+ the current directory (written by 'cm init'). Otherwise pass it explicitly.
64
+
65
+ cm start [project]
60
66
  Load all unresolved memories for a project, sorted by decay
61
67
  score and capped at a 2000 token context budget, grouped by
62
68
  type (decisions, constraints, discoveries, rejections), with
63
69
  linked memory ids shown under each entry.
64
70
 
65
- cm save <project> <type> <content> [--force]
71
+ cm save [project] <type> <content> [--force]
66
72
  Save a new memory. <type> is one of: decision, rejection,
67
73
  constraint, discovery. Automatically links to related existing
68
74
  memories. If the new memory contradicts an existing one, prompts
69
75
  to keep the old memory or replace it — pass --force to skip the
70
76
  prompt and always replace.
71
77
 
72
- cm fix <project> <problem> <solution>
78
+ cm fix [project] <problem> <solution>
73
79
  Record a problem and its solution for future reference.
74
80
 
75
- cm analyze <project> <path>
81
+ cm analyze [project] <path>
76
82
  Build an AST index (files, functions, classes) for the codebase
77
83
  at <path>.
78
84
 
79
- cm search <project> <query>
85
+ cm search [project] <query>
80
86
  Search saved memories and past fixes for a project matching
81
87
  <query>.
82
88
 
83
- cm resolve <project> <memory-id>
89
+ cm resolve [project] <memory-id>
84
90
  Mark a memory as resolved.
85
91
 
86
- cm delete <project> <memory-id>
92
+ cm delete [project] <memory-id>
87
93
  Delete a memory and any memory_links referencing it.
88
94
 
89
- cm compress <project>
95
+ cm compress [project]
90
96
  Cluster related unresolved memories (needs 20+) and collapse
91
97
  clusters of 3 or more into a single summary memory.
92
98
 
93
99
  cm init <project> <path>
94
- Install a git post-commit hook in the repo at <path> that
95
- records the commit message and any implicit memories (removed
96
- imports, new env vars, new auth-related files) after every
97
- commit.
100
+ Install a git post-commit hook in the repo at <path>, register
101
+ the stackmem MCP server with Claude Code, write a CLAUDE.md and
102
+ .stackmem file, and seed initial memories from the project scan.
98
103
 
99
- cm context <project> <task>
104
+ cm context [project] <task>
100
105
  Return only the top 5 memories most relevant to a specific
101
106
  task, ranked by a blend of task relevance and decay score,
102
107
  as a markdown block capped at a 2000 token budget.
@@ -110,6 +115,53 @@ function fail(message: string): never {
110
115
  process.exit(1);
111
116
  }
112
117
 
118
+ // --- optional project resolution ------------------------------------------
119
+
120
+ const STACKMEM_FILE = ".stackmem";
121
+
122
+ function readProjectFromFile(): string | null {
123
+ const stackmemPath = nodePath.join(process.cwd(), STACKMEM_FILE);
124
+ if (!nodeFs.existsSync(stackmemPath)) return null;
125
+ const content = nodeFs.readFileSync(stackmemPath, "utf-8").trim();
126
+ return content.length > 0 ? content : null;
127
+ }
128
+
129
+ function requireProject(explicit: string | null): string {
130
+ if (explicit) return explicit;
131
+
132
+ const fromFile = readProjectFromFile();
133
+ if (fromFile) return fromFile;
134
+
135
+ console.error(
136
+ "No project specified and no .stackmem file found. Run 'cm init <project> .' first or pass a project name."
137
+ );
138
+ process.exit(1);
139
+ }
140
+
141
+ // For commands whose non-project args have a fixed count: if exactly that
142
+ // many args are given, project was omitted; if one more, the first is the
143
+ // project. Anything else is a usage error.
144
+ function splitOptionalProject(
145
+ args: string[],
146
+ fixedArgCount: number
147
+ ): { project: string | null; rest: string[] } | null {
148
+ if (args.length === fixedArgCount) return { project: null, rest: args };
149
+ if (args.length === fixedArgCount + 1) return { project: args[0], rest: args.slice(1) };
150
+ return null;
151
+ }
152
+
153
+ // For commands whose remaining arg is free-form text (a query or task),
154
+ // arg count can't disambiguate an omitted project from a multi-word query —
155
+ // "cm search bug fix" is ambiguous by count alone. Use .stackmem's presence
156
+ // instead: if it exists, nothing is positionally a project.
157
+ function splitProjectFromVariadic(args: string[]): { project: string | null; rest: string[] } {
158
+ if (readProjectFromFile() !== null) {
159
+ return { project: null, rest: args };
160
+ }
161
+ const [project = null, ...rest] = args;
162
+ return { project, rest };
163
+ }
164
+
113
165
  // --- cm start ----------------------------------------------------------
114
166
 
115
167
  async function cmdStart(project: string) {
@@ -471,11 +523,20 @@ async function cmdDelete(project: string, memoryId: string) {
471
523
  // .git/hooks/post-commit) so its module type is unambiguous — an
472
524
  // extensionless file run by git would otherwise have its CommonJS/ESM
473
525
  // interpretation depend on the target repo's own package.json.
474
- function buildHookLogicScript(project: string, cliPath: string): string {
526
+ function buildHookLogicScript(cliPath: string): string {
475
527
  return `#!/usr/bin/env node
476
528
  import { execFileSync } from "node:child_process";
529
+ import { readFileSync } from "node:fs";
530
+ import { join } from "node:path";
531
+
532
+ const stackmemPath = join(process.cwd(), ".stackmem");
533
+ let PROJECT;
534
+ try {
535
+ PROJECT = readFileSync(stackmemPath, "utf8").trim();
536
+ } catch {
537
+ process.exit(0); // no .stackmem, skip silently
538
+ }
477
539
 
478
- const PROJECT = ${JSON.stringify(project)};
479
540
  const CLI_PATH = ${JSON.stringify(cliPath)};
480
541
 
481
542
  function git(args) {
@@ -703,6 +764,75 @@ cm save ${project} <type> "<content>"
703
764
  console.log("CLAUDE.md written.");
704
765
  }
705
766
 
767
+ function writeProjectFile(project: string, resolvedPath: string): void {
768
+ const stackmemPath = nodePath.join(resolvedPath, STACKMEM_FILE);
769
+ nodeFs.writeFileSync(stackmemPath, `${project}\n`, "utf-8");
770
+ }
771
+
772
+ // Scans the freshly-initialized project for a few cheap, high-signal facts
773
+ // and saves them as memories directly (bypassing cmdSave's duplicate/conflict
774
+ // prompt — there's nothing to conflict with yet, and init must stay
775
+ // non-interactive). Returns how many memories were actually saved.
776
+ async function seedMemories(project: string, resolvedPath: string): Promise<number> {
777
+ let seededCount = 0;
778
+
779
+ const packageJsonPath = nodePath.join(resolvedPath, "package.json");
780
+ if (nodeFs.existsSync(packageJsonPath)) {
781
+ try {
782
+ const pkg = JSON.parse(nodeFs.readFileSync(packageJsonPath, "utf-8"));
783
+ const depNames = [
784
+ ...Object.keys(pkg.dependencies ?? {}),
785
+ ...Object.keys(pkg.devDependencies ?? {}),
786
+ ];
787
+ if (depNames.length > 0) {
788
+ const { error } = await supabase.from("memories").insert({
789
+ project,
790
+ type: "discovery",
791
+ content: `tech stack: ${depNames.join(", ")}`,
792
+ device_id: getDeviceId(),
793
+ });
794
+ if (!error) seededCount++;
795
+ }
796
+ } catch {
797
+ // Malformed package.json — skip the tech-stack memory.
798
+ }
799
+ }
800
+
801
+ const envExamplePath = nodePath.join(resolvedPath, ".env.example");
802
+ if (nodeFs.existsSync(envExamplePath)) {
803
+ const varNames = nodeFs
804
+ .readFileSync(envExamplePath, "utf-8")
805
+ .split("\n")
806
+ .filter((line) => /^[A-Z_]+=/.test(line))
807
+ .map((line) => line.split("=")[0]);
808
+
809
+ if (varNames.length > 0) {
810
+ const { error } = await supabase.from("memories").insert({
811
+ project,
812
+ type: "constraint",
813
+ content: `required env vars: ${varNames.join(", ")}`,
814
+ device_id: getDeviceId(),
815
+ });
816
+ if (!error) seededCount++;
817
+ }
818
+ }
819
+
820
+ analyzeCodebase(resolvedPath);
821
+
822
+ return seededCount;
823
+ }
824
+
825
+ // Smoke-tests the same anon-key + device-header path session_start/cmdStart
826
+ // use, without pulling in all of cmdStart's scoring/formatting logic.
827
+ async function verifyBackendConnection(project: string): Promise<boolean> {
828
+ const { error } = await supabase
829
+ .from("memories")
830
+ .select("id", { count: "exact", head: true })
831
+ .eq("project", project);
832
+
833
+ return !error;
834
+ }
835
+
706
836
  async function cmdInit(project: string, targetPath: string) {
707
837
  const resolvedPath = nodePath.resolve(targetPath);
708
838
  const gitDir = nodePath.join(resolvedPath, ".git");
@@ -716,7 +846,7 @@ async function cmdInit(project: string, targetPath: string) {
716
846
  nodeFs.mkdirSync(codingMemoryDir, { recursive: true });
717
847
 
718
848
  const hookLogicPath = nodePath.join(codingMemoryDir, "post-commit-hook.mjs");
719
- nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(project, DIST_CLI_PATH), "utf-8");
849
+ nodeFs.writeFileSync(hookLogicPath, buildHookLogicScript(DIST_CLI_PATH), "utf-8");
720
850
 
721
851
  const hooksDir = nodePath.join(gitDir, "hooks");
722
852
  nodeFs.mkdirSync(hooksDir, { recursive: true });
@@ -729,6 +859,31 @@ async function cmdInit(project: string, targetPath: string) {
729
859
 
730
860
  registerMcpServer(DIST_INDEX_PATH);
731
861
  writeClaudeMd(project, resolvedPath);
862
+
863
+ const seededCount = await seedMemories(project, resolvedPath);
864
+ console.log(`Seeded ${seededCount} initial memories from project scan.`);
865
+
866
+ const backendOk = await verifyBackendConnection(project);
867
+
868
+ console.log("");
869
+ console.log("✓ Git hook installed");
870
+ console.log("✓ MCP server registered");
871
+ console.log("✓ CLAUDE.md written");
872
+ console.log(`✓ Seeded ${seededCount} memories from project scan`);
873
+ if (backendOk) {
874
+ console.log("✓ Backend connection verified");
875
+ console.log("");
876
+ console.log(
877
+ `stackmem is live. Open Claude Code and ask "what do you know about this project?" to verify.`
878
+ );
879
+ } else {
880
+ console.log("✗ Backend connection failed — run 'cm doctor' (once we build it)");
881
+ }
882
+
883
+ // Written last and deliberately: if a commit (and hence the post-commit
884
+ // hook) fires anywhere during init, it must still see whatever project
885
+ // .stackmem pointed to before this run, not the one being initialized now.
886
+ writeProjectFile(project, resolvedPath);
732
887
  }
733
888
 
734
889
  // --- cm context ------------------------------------------------------------
@@ -794,8 +949,9 @@ async function main() {
794
949
 
795
950
  switch (command) {
796
951
  case "start": {
797
- const [project] = args;
798
- if (!project) usage();
952
+ const split = splitOptionalProject(args, 0);
953
+ if (!split) usage();
954
+ const project = requireProject(split.project);
799
955
  await cmdStart(project);
800
956
  break;
801
957
  }
@@ -805,44 +961,66 @@ async function main() {
805
961
  const positional = force
806
962
  ? [...args.slice(0, forceIndex), ...args.slice(forceIndex + 1)]
807
963
  : args;
808
- const [project, type, ...rest] = positional;
809
- if (!project || !type || rest.length === 0) usage();
964
+
965
+ let explicitProject: string | null;
966
+ let type: string | undefined;
967
+ let rest: string[];
968
+ if (MEMORY_TYPES.includes(positional[0] as MemoryType)) {
969
+ explicitProject = null;
970
+ [type, ...rest] = positional;
971
+ } else {
972
+ explicitProject = positional[0] ?? null;
973
+ [, type, ...rest] = positional;
974
+ }
975
+
976
+ if (!type || rest.length === 0) usage();
977
+ const project = requireProject(explicitProject);
810
978
  await cmdSave(project, type, rest.join(" "), force);
811
979
  break;
812
980
  }
813
981
  case "fix": {
814
- const [project, problem, solution] = args;
815
- if (!project || !problem || !solution) usage();
982
+ const split = splitOptionalProject(args, 2);
983
+ if (!split) usage();
984
+ const project = requireProject(split.project);
985
+ const [problem, solution] = split.rest;
816
986
  await cmdFix(project, problem, solution);
817
987
  break;
818
988
  }
819
989
  case "analyze": {
820
- const [project, path] = args;
821
- if (!project || !path) usage();
990
+ const split = splitOptionalProject(args, 1);
991
+ if (!split) usage();
992
+ const project = requireProject(split.project);
993
+ const [path] = split.rest;
822
994
  await cmdAnalyze(project, path);
823
995
  break;
824
996
  }
825
997
  case "search": {
826
- const [project, ...rest] = args;
827
- if (!project || rest.length === 0) usage();
828
- await cmdSearch(project, rest.join(" "));
998
+ const split = splitProjectFromVariadic(args);
999
+ const project = requireProject(split.project);
1000
+ if (split.rest.length === 0) usage();
1001
+ await cmdSearch(project, split.rest.join(" "));
829
1002
  break;
830
1003
  }
831
1004
  case "resolve": {
832
- const [project, memoryId] = args;
833
- if (!project || !memoryId) usage();
1005
+ const split = splitOptionalProject(args, 1);
1006
+ if (!split) usage();
1007
+ const project = requireProject(split.project);
1008
+ const [memoryId] = split.rest;
834
1009
  await cmdResolve(project, memoryId);
835
1010
  break;
836
1011
  }
837
1012
  case "delete": {
838
- const [project, memoryId] = args;
839
- if (!project || !memoryId) usage();
1013
+ const split = splitOptionalProject(args, 1);
1014
+ if (!split) usage();
1015
+ const project = requireProject(split.project);
1016
+ const [memoryId] = split.rest;
840
1017
  await cmdDelete(project, memoryId);
841
1018
  break;
842
1019
  }
843
1020
  case "compress": {
844
- const [project] = args;
845
- if (!project) usage();
1021
+ const split = splitOptionalProject(args, 0);
1022
+ if (!split) usage();
1023
+ const project = requireProject(split.project);
846
1024
  await cmdCompress(project);
847
1025
  break;
848
1026
  }
@@ -853,9 +1031,10 @@ async function main() {
853
1031
  break;
854
1032
  }
855
1033
  case "context": {
856
- const [project, ...rest] = args;
857
- if (!project || rest.length === 0) usage();
858
- await cmdContext(project, rest.join(" "));
1034
+ const split = splitProjectFromVariadic(args);
1035
+ const project = requireProject(split.project);
1036
+ if (split.rest.length === 0) usage();
1037
+ await cmdContext(project, split.rest.join(" "));
859
1038
  break;
860
1039
  }
861
1040
  case "help": {