@lawaty/lacode 0.1.3 → 0.1.5

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/agents.js CHANGED
@@ -33,6 +33,7 @@ var PRESETS_DIR = join(PACKAGE_ROOT, "presets");
33
33
  var DEFAULT_PRESET = join(PRESETS_DIR, "free-tier.json");
34
34
  var USER_CONFIG_DIR = join(homedir(), ".config", "lacode");
35
35
  var USER_MODELS_PATH = join(USER_CONFIG_DIR, "pool.json");
36
+ var USER_MODELS_TXT = join(USER_CONFIG_DIR, "pool.txt");
36
37
 
37
38
  // src/agents.ts
38
39
  var POOLED_BASES = ["explore", "implement", "context"];
@@ -116,7 +117,9 @@ missing). Never paste context file contents into the report.`,
116
117
  "task": {
117
118
  "*": "deny",
118
119
  "explore*": "allow",
119
- "implement*": "allow"
120
+ "implement*": "allow",
121
+ "basic": "allow",
122
+ "expert": "allow"
120
123
  }
121
124
  }
122
125
  },
@@ -186,7 +189,9 @@ Once the plan is complete, present it clearly so the caller can implement it ste
186
189
  "task": {
187
190
  "*": "deny",
188
191
  "explore*": "allow",
189
- "implement*": "allow"
192
+ "implement*": "allow",
193
+ "basic": "allow",
194
+ "expert": "allow"
190
195
  }
191
196
  }
192
197
  },
package/dist/index.js CHANGED
@@ -35,6 +35,7 @@ var PRESETS_DIR = join(PACKAGE_ROOT, "presets");
35
35
  var DEFAULT_PRESET = join(PRESETS_DIR, "free-tier.json");
36
36
  var USER_CONFIG_DIR = join(homedir(), ".config", "lacode");
37
37
  var USER_MODELS_PATH = join(USER_CONFIG_DIR, "pool.json");
38
+ var USER_MODELS_TXT = join(USER_CONFIG_DIR, "pool.txt");
38
39
  var FALLBACK_SLOTS = [
39
40
  { index: 1, model: "opencode/space-bunny-free", weight: 3 },
40
41
  { index: 2, model: "openrouter/nvidia/nemotron-3-ultra-550b-a55b:free", weight: 2 },
@@ -66,6 +67,26 @@ function parseSlots(parsed, origin) {
66
67
  }
67
68
  return { slots, problems };
68
69
  }
70
+ function parseTextSlots(text, origin) {
71
+ const entries = [];
72
+ for (const raw of text.split(/\r?\n/)) {
73
+ const line = raw.split("#")[0].trim();
74
+ if (!line) continue;
75
+ const [model, weight] = line.split(/\s+/);
76
+ entries.push(weight === void 0 ? { model } : { model, weight });
77
+ }
78
+ return parseSlots({ slots: entries }, origin);
79
+ }
80
+ function parseSlotText(text, origin) {
81
+ if (text.trimStart().startsWith("{")) {
82
+ try {
83
+ return parseSlots(JSON.parse(text), origin);
84
+ } catch (error) {
85
+ return { slots: FALLBACK_SLOTS, problems: [`${origin} is not valid JSON (${String(error)}); using built-in defaults`] };
86
+ }
87
+ }
88
+ return parseTextSlots(text, origin);
89
+ }
69
90
  var readJSON = (path4) => {
70
91
  try {
71
92
  return { ok: true, value: JSON.parse(readFileSync(path4, "utf8")) };
@@ -81,6 +102,15 @@ function readOptional(path4, origin, problems) {
81
102
  }
82
103
  return result.value;
83
104
  }
105
+ function readTextOptional(path4, origin, problems) {
106
+ if (!existsSync(path4)) return void 0;
107
+ try {
108
+ return readFileSync(path4, "utf8");
109
+ } catch (error) {
110
+ problems.push(`${origin} unreadable (${String(error)}); trying the next source`);
111
+ return void 0;
112
+ }
113
+ }
84
114
  function resolveModels(input = {}) {
85
115
  const problems = [];
86
116
  if (input.models !== void 0) {
@@ -92,11 +122,15 @@ function resolveModels(input = {}) {
92
122
  return { slots: parsed.slots, problems: [...problems, ...parsed.problems], origin };
93
123
  }
94
124
  if (input.modelsFile) {
95
- const result = readJSON(input.modelsFile);
96
- if (!result.ok) return { slots: FALLBACK_SLOTS, problems: [`${input.modelsFile} unreadable (${result.error}); using built-in defaults`], origin: input.modelsFile };
97
- const parsed = parseSlots(result.value, input.modelsFile);
125
+ const parsed = readSlotFile(input.modelsFile);
98
126
  return { slots: parsed.slots, problems: parsed.problems, origin: input.modelsFile };
99
127
  }
128
+ const text = readTextOptional(USER_MODELS_TXT, USER_MODELS_TXT, problems);
129
+ if (text !== void 0) {
130
+ const parsed = parseSlotText(text, USER_MODELS_TXT);
131
+ if (parsed.slots !== FALLBACK_SLOTS) return { slots: parsed.slots, problems: [...problems, ...parsed.problems], origin: USER_MODELS_TXT };
132
+ problems.push(...parsed.problems);
133
+ }
100
134
  const user = readOptional(USER_MODELS_PATH, USER_MODELS_PATH, problems);
101
135
  if (user !== void 0) {
102
136
  const parsed = parseSlots(user, USER_MODELS_PATH);
@@ -108,7 +142,16 @@ function resolveModels(input = {}) {
108
142
  const parsed = parseSlots(preset, "presets/free-tier.json");
109
143
  return { slots: parsed.slots, problems: [...problems, ...parsed.problems], origin: "presets/free-tier.json" };
110
144
  }
111
- return { slots: FALLBACK_SLOTS, problems: [...problems, `no pool source found (${USER_MODELS_PATH}, ${DEFAULT_PRESET}); using built-in defaults`], origin: "built-in defaults" };
145
+ return { slots: FALLBACK_SLOTS, problems: [...problems, `no pool source found (${USER_MODELS_TXT}, ${USER_MODELS_PATH}, ${DEFAULT_PRESET}); using built-in defaults`], origin: "built-in defaults" };
146
+ }
147
+ function readSlotFile(path4) {
148
+ let text;
149
+ try {
150
+ text = readFileSync(path4, "utf8");
151
+ } catch (error) {
152
+ return { slots: FALLBACK_SLOTS, problems: [`${path4} unreadable (${String(error)}); using built-in defaults`] };
153
+ }
154
+ return parseSlotText(text, path4);
112
155
  }
113
156
  function ensureUserConfigDir() {
114
157
  try {
@@ -528,7 +571,9 @@ missing). Never paste context file contents into the report.`,
528
571
  "task": {
529
572
  "*": "deny",
530
573
  "explore*": "allow",
531
- "implement*": "allow"
574
+ "implement*": "allow",
575
+ "basic": "allow",
576
+ "expert": "allow"
532
577
  }
533
578
  }
534
579
  },
@@ -598,7 +643,9 @@ Once the plan is complete, present it clearly so the caller can implement it ste
598
643
  "task": {
599
644
  "*": "deny",
600
645
  "explore*": "allow",
601
- "implement*": "allow"
646
+ "implement*": "allow",
647
+ "basic": "allow",
648
+ "expert": "allow"
602
649
  }
603
650
  }
604
651
  },
package/dist/pool.js CHANGED
@@ -35,6 +35,7 @@ var PRESETS_DIR = join(PACKAGE_ROOT, "presets");
35
35
  var DEFAULT_PRESET = join(PRESETS_DIR, "free-tier.json");
36
36
  var USER_CONFIG_DIR = join(homedir(), ".config", "lacode");
37
37
  var USER_MODELS_PATH = join(USER_CONFIG_DIR, "pool.json");
38
+ var USER_MODELS_TXT = join(USER_CONFIG_DIR, "pool.txt");
38
39
  var FALLBACK_SLOTS = [
39
40
  { index: 1, model: "opencode/space-bunny-free", weight: 3 },
40
41
  { index: 2, model: "openrouter/nvidia/nemotron-3-ultra-550b-a55b:free", weight: 2 },
@@ -66,6 +67,26 @@ function parseSlots(parsed, origin) {
66
67
  }
67
68
  return { slots, problems };
68
69
  }
70
+ function parseTextSlots(text, origin) {
71
+ const entries = [];
72
+ for (const raw of text.split(/\r?\n/)) {
73
+ const line = raw.split("#")[0].trim();
74
+ if (!line) continue;
75
+ const [model, weight] = line.split(/\s+/);
76
+ entries.push(weight === void 0 ? { model } : { model, weight });
77
+ }
78
+ return parseSlots({ slots: entries }, origin);
79
+ }
80
+ function parseSlotText(text, origin) {
81
+ if (text.trimStart().startsWith("{")) {
82
+ try {
83
+ return parseSlots(JSON.parse(text), origin);
84
+ } catch (error) {
85
+ return { slots: FALLBACK_SLOTS, problems: [`${origin} is not valid JSON (${String(error)}); using built-in defaults`] };
86
+ }
87
+ }
88
+ return parseTextSlots(text, origin);
89
+ }
69
90
  var readJSON = (path) => {
70
91
  try {
71
92
  return { ok: true, value: JSON.parse(readFileSync(path, "utf8")) };
@@ -81,6 +102,15 @@ function readOptional(path, origin, problems) {
81
102
  }
82
103
  return result.value;
83
104
  }
105
+ function readTextOptional(path, origin, problems) {
106
+ if (!existsSync(path)) return void 0;
107
+ try {
108
+ return readFileSync(path, "utf8");
109
+ } catch (error) {
110
+ problems.push(`${origin} unreadable (${String(error)}); trying the next source`);
111
+ return void 0;
112
+ }
113
+ }
84
114
  function resolveModels(input = {}) {
85
115
  const problems = [];
86
116
  if (input.models !== void 0) {
@@ -92,11 +122,15 @@ function resolveModels(input = {}) {
92
122
  return { slots: parsed.slots, problems: [...problems, ...parsed.problems], origin };
93
123
  }
94
124
  if (input.modelsFile) {
95
- const result = readJSON(input.modelsFile);
96
- if (!result.ok) return { slots: FALLBACK_SLOTS, problems: [`${input.modelsFile} unreadable (${result.error}); using built-in defaults`], origin: input.modelsFile };
97
- const parsed = parseSlots(result.value, input.modelsFile);
125
+ const parsed = readSlotFile(input.modelsFile);
98
126
  return { slots: parsed.slots, problems: parsed.problems, origin: input.modelsFile };
99
127
  }
128
+ const text = readTextOptional(USER_MODELS_TXT, USER_MODELS_TXT, problems);
129
+ if (text !== void 0) {
130
+ const parsed = parseSlotText(text, USER_MODELS_TXT);
131
+ if (parsed.slots !== FALLBACK_SLOTS) return { slots: parsed.slots, problems: [...problems, ...parsed.problems], origin: USER_MODELS_TXT };
132
+ problems.push(...parsed.problems);
133
+ }
100
134
  const user = readOptional(USER_MODELS_PATH, USER_MODELS_PATH, problems);
101
135
  if (user !== void 0) {
102
136
  const parsed = parseSlots(user, USER_MODELS_PATH);
@@ -108,12 +142,16 @@ function resolveModels(input = {}) {
108
142
  const parsed = parseSlots(preset, "presets/free-tier.json");
109
143
  return { slots: parsed.slots, problems: [...problems, ...parsed.problems], origin: "presets/free-tier.json" };
110
144
  }
111
- return { slots: FALLBACK_SLOTS, problems: [...problems, `no pool source found (${USER_MODELS_PATH}, ${DEFAULT_PRESET}); using built-in defaults`], origin: "built-in defaults" };
145
+ return { slots: FALLBACK_SLOTS, problems: [...problems, `no pool source found (${USER_MODELS_TXT}, ${USER_MODELS_PATH}, ${DEFAULT_PRESET}); using built-in defaults`], origin: "built-in defaults" };
112
146
  }
113
147
  function readSlotFile(path) {
114
- const result = readJSON(path);
115
- if (!result.ok) return { slots: FALLBACK_SLOTS, problems: [`${path} unreadable (${result.error}); using built-in defaults`] };
116
- return parseSlots(result.value, path);
148
+ let text;
149
+ try {
150
+ text = readFileSync(path, "utf8");
151
+ } catch (error) {
152
+ return { slots: FALLBACK_SLOTS, problems: [`${path} unreadable (${String(error)}); using built-in defaults`] };
153
+ }
154
+ return parseSlotText(text, path);
117
155
  }
118
156
  function ensureUserConfigDir() {
119
157
  try {
@@ -471,6 +509,7 @@ export {
471
509
  UNAVAILABLE_COOLDOWN_MS,
472
510
  USER_CONFIG_DIR,
473
511
  USER_MODELS_PATH,
512
+ USER_MODELS_TXT,
474
513
  acquire,
475
514
  agentFor,
476
515
  bindChild,
@@ -486,7 +525,9 @@ export {
486
525
  isLimitText,
487
526
  isUnavailableText,
488
527
  noteActivity,
528
+ parseSlotText,
489
529
  parseSlots,
530
+ parseTextSlots,
490
531
  pickSlot,
491
532
  providerOf,
492
533
  pruneOwn,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lawaty/lacode",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Ultimate opencode setup for Go users — cost-controlled pooled subagents, hang recovery, and notifications for opencode.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/presets/README.md CHANGED
@@ -10,7 +10,7 @@ LaCode reads the first source that yields usable slots, in this order:
10
10
  | # | Source | Where |
11
11
  |---|--------|-------|
12
12
  | 1 | plugin options | `["@lawaty/lacode", { "models": { "slots": [...] } }]` |
13
- | 2 | user config | `~/.config/lacode/pool.json` |
13
+ | 2 | user config | `~/.config/lacode/pool.txt` (flat, preferred) or `~/.config/lacode/pool.json` (JSON) |
14
14
  | 3 | bundled preset | `presets/free-tier.json` |
15
15
  | 4 | built-in fallback | hardcoded in `src/lib/pool.ts`, so routing always works |
16
16
 
@@ -29,6 +29,25 @@ Source 3 is a default, not a floor.
29
29
  Nothing is hot-reloaded. Change a preset, then restart every running opencode
30
30
  process.
31
31
 
32
+ ## The flat pool file (recommended)
33
+
34
+ `~/.config/lacode/pool.txt` is one model per line — edit a model id or a weight
35
+ in place, no JSON and no objects:
36
+
37
+ ```text
38
+ # provider/model-id weight
39
+ opencode-go/longcat-2.5-preview-free 3
40
+ opencode/space-bunny-free 2
41
+ opencode/big-pickle 1
42
+ opencode/longcat-2.5-preview-free 1
43
+ ```
44
+
45
+ `#` starts a comment (whole-line or trailing), blank lines are ignored, and the
46
+ weight is optional (default 1). The rules are exactly the same as the JSON form
47
+ below: up to 4 slots, distinct models, split on the first slash only. The loader
48
+ accepts either shape in the same file, so a leading `{` means JSON and anything
49
+ else means the flat form.
50
+
32
51
  ## Schema
33
52
 
34
53
  ```jsonc
@@ -89,8 +108,8 @@ provider-wide rate limit takes out every slot on that provider at once.
89
108
 
90
109
  ## Writing your own
91
110
 
92
- Copy `free-tier.json`, change the slots, and put it at
93
- `~/.config/lacode/pool.json` — or skip the file entirely and pass `models`
111
+ Write `~/.config/lacode/pool.txt` (the flat form above) — that is the whole job.
112
+ Or put the same slots in a JSON `~/.config/lacode/pool.json`, or pass `models`
94
113
  inline from `opencode.jsonc` when you want the list version-controlled beside
95
114
  the rest of your config:
96
115
 
@@ -52,8 +52,8 @@
52
52
  "Changing this file requires restarting every opencode process: plugins are not hot-reloaded."
53
53
  ],
54
54
  "slots": [
55
- { "model": "opencode-go/longcat-2.5-preview-free", "weight": 3 },
56
- { "model": "opencode/space-bunny-free", "weight": 2 },
55
+ { "model": "opencode/space-bunny-free", "weight": 3 },
56
+ { "model": "opencode-go/longcat-2.5-preview-free", "weight": 2 },
57
57
  { "model": "opencode/big-pickle", "weight": 1 },
58
58
  { "model": "opencode/longcat-2.5-preview-free", "weight": 1 }
59
59
  ]