@commonlyai/cli 0.1.86 → 0.1.88
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/package.json +1 -1
- package/src/commands/agent.js +33 -1
- package/src/lib/enforcement.js +66 -25
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -127,6 +127,30 @@ export const deleteAgentToken = (name) => {
|
|
|
127
127
|
// falls back to the attach hint).
|
|
128
128
|
export const BOOTSTRAP_ADAPTER_DETECT_ORDER = ['claude', 'codex', 'pi'];
|
|
129
129
|
|
|
130
|
+
// ── `agent run --adapter` against an existing token file (#2098) ────────────
|
|
131
|
+
// The flag picks the CLI once, at first-run bootstrap; after that the token
|
|
132
|
+
// file's adapter is what runs. A different `--adapter` used to be dropped
|
|
133
|
+
// without a word, so "switch this agent to Codex" kept running Claude and read
|
|
134
|
+
// as Codex misbehaving. Refuse instead, and name the way out for each path:
|
|
135
|
+
// a foreground run re-bootstraps from the Connect page's env exports after the
|
|
136
|
+
// local record is removed; a daemon-managed seat is switched through the
|
|
137
|
+
// registry, which the daemon applies on its next pass.
|
|
138
|
+
export const checkRunAdapterRequest = ({ record, requestedAdapter, tokenPath }) => {
|
|
139
|
+
const requested = String(requestedAdapter || '').trim().toLowerCase();
|
|
140
|
+
if (!requested) return { ok: true };
|
|
141
|
+
const bound = String(record?.adapter || '').trim().toLowerCase();
|
|
142
|
+
if (!bound || bound === requested) return { ok: true };
|
|
143
|
+
const name = record.agentName;
|
|
144
|
+
return {
|
|
145
|
+
ok: false,
|
|
146
|
+
message: [
|
|
147
|
+
`'${name}' is bound to the ${bound} adapter by its token file (${tokenPath}); --adapter ${requested} was not applied.`,
|
|
148
|
+
`To run it with ${requested} here: commonly agent detach ${name} --force, then re-run with the Connect-page env exports and --adapter ${requested}.`,
|
|
149
|
+
`A daemon-managed seat switches with: commonly agent config ${name} --adapter ${requested}`,
|
|
150
|
+
].join('\n'),
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
130
154
|
export const bootstrapAgentRecordFromEnv = async ({
|
|
131
155
|
name,
|
|
132
156
|
env = process.env,
|
|
@@ -2690,7 +2714,7 @@ Docs:
|
|
|
2690
2714
|
.command('run <name>')
|
|
2691
2715
|
.description('Run the local-CLI wrapper loop for an attached agent')
|
|
2692
2716
|
.option('--interval <ms>', 'Poll interval in ms', '5000')
|
|
2693
|
-
.option('--adapter <name>', 'CLI to wrap on first-run bootstrap (claude|codex);
|
|
2717
|
+
.option('--adapter <name>', 'CLI to wrap on first-run bootstrap (claude|codex); an existing token file bound to a different adapter stops the run')
|
|
2694
2718
|
.option('--cascade-cap <n>', `Consecutive agent-triggered turns allowed per pod (env ${CASCADE_ENV_VARS.cap}, default ${CASCADE_DEFAULTS.cap})`)
|
|
2695
2719
|
.option('--cascade-grace <n>', `Extra turns allowed when this seat was directly addressed; 0 disables the grace (env ${CASCADE_ENV_VARS.addressedGrace}, default ${CASCADE_DEFAULTS.addressedGrace})`)
|
|
2696
2720
|
.option('--cascade-reset <ms>', `Silence window that clears the streak (env ${CASCADE_ENV_VARS.resetMs}, default ${CASCADE_DEFAULTS.resetMs})`)
|
|
@@ -2722,6 +2746,14 @@ Docs:
|
|
|
2722
2746
|
}
|
|
2723
2747
|
}
|
|
2724
2748
|
|
|
2749
|
+
const adapterRequest = checkRunAdapterRequest({
|
|
2750
|
+
record, requestedAdapter: opts.adapter, tokenPath: tokenFile(name),
|
|
2751
|
+
});
|
|
2752
|
+
if (!adapterRequest.ok) {
|
|
2753
|
+
console.error(`${stamp()} [${name}] ${adapterRequest.message}`);
|
|
2754
|
+
process.exit(1);
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2725
2757
|
const adapter = getAdapter(record.adapter);
|
|
2726
2758
|
if (!adapter) {
|
|
2727
2759
|
console.error(`Unknown adapter '${record.adapter}' in token file. Known: ${listAdapterNames().join(', ')}`);
|
package/src/lib/enforcement.js
CHANGED
|
@@ -483,12 +483,53 @@ export const createClaimKeeper = (client, {
|
|
|
483
483
|
|
|
484
484
|
// ── post-time length gate ───────────────────────────────────────────────────
|
|
485
485
|
|
|
486
|
+
// A sentence ends at [.!?] followed by whitespace, except when what precedes
|
|
487
|
+
// the whitespace is only a numbered list marker at the start of the line: in
|
|
488
|
+
// "3. Run it once." the first period belongs to the marker, not a sentence.
|
|
489
|
+
// Applied to one line at a time, so `^` is the line start.
|
|
490
|
+
const SENTENCE_END = /(?<=[.!?])(?<!^[ \t]*\d{1,3}\.)\s+/;
|
|
491
|
+
|
|
492
|
+
// Split one over-limit line at sentence boundaries, and a sentence that is
|
|
493
|
+
// itself over the limit at word boundaries. A single over-limit word (a URL)
|
|
494
|
+
// posts whole rather than being cut.
|
|
495
|
+
const splitLineForChat = (line, limit) => {
|
|
496
|
+
const units = [];
|
|
497
|
+
let piece = '';
|
|
498
|
+
const flushPiece = () => {
|
|
499
|
+
if (piece.trim()) units.push(piece.trim());
|
|
500
|
+
piece = '';
|
|
501
|
+
};
|
|
502
|
+
for (const sentence of line.split(SENTENCE_END)) {
|
|
503
|
+
if (sentence.length > limit) {
|
|
504
|
+
flushPiece();
|
|
505
|
+
let run = '';
|
|
506
|
+
for (const word of sentence.split(/\s+/)) {
|
|
507
|
+
if (run && `${run} ${word}`.length > limit) {
|
|
508
|
+
units.push(run);
|
|
509
|
+
run = word;
|
|
510
|
+
} else {
|
|
511
|
+
run = run ? `${run} ${word}` : word;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (run) units.push(run);
|
|
515
|
+
} else if (piece && `${piece} ${sentence}`.length > limit) {
|
|
516
|
+
flushPiece();
|
|
517
|
+
piece = sentence;
|
|
518
|
+
} else {
|
|
519
|
+
piece = piece ? `${piece} ${sentence}` : sentence;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
flushPiece();
|
|
523
|
+
return units;
|
|
524
|
+
};
|
|
525
|
+
|
|
486
526
|
/**
|
|
487
527
|
* Split chat text into tone-contract-sized messages without ever cutting
|
|
488
528
|
* content. Boundaries in preference order: fenced code blocks stay whole
|
|
489
529
|
* (atomic — an oversized fence becomes one oversized message rather than a
|
|
490
|
-
* broken pair), then paragraphs, then
|
|
491
|
-
*
|
|
530
|
+
* broken pair), then paragraphs, then lines (a list item never leaves its
|
|
531
|
+
* marker), then sentences, then words. Greedy packing rejoins small pieces so
|
|
532
|
+
* two short paragraphs share one message.
|
|
492
533
|
*/
|
|
493
534
|
export const splitForChat = (text, { limit = 400 } = {}) => {
|
|
494
535
|
const trimmed = String(text || '').trim();
|
|
@@ -529,39 +570,39 @@ export const splitForChat = (text, { limit = 400 } = {}) => {
|
|
|
529
570
|
}
|
|
530
571
|
flushProse();
|
|
531
572
|
|
|
532
|
-
// Pass 2: split oversized prose blocks at sentence then word
|
|
573
|
+
// Pass 2: split oversized prose blocks at line, then sentence, then word
|
|
574
|
+
// boundaries. Lines come first because a list is one paragraph block whose
|
|
575
|
+
// items are separated by single newlines: splitting it by sentence read a
|
|
576
|
+
// numbered marker ("3.") as a sentence end and packed it onto the previous
|
|
577
|
+
// piece, so a support reply posted one message ending in "3." and the next
|
|
578
|
+
// opening with step 3's body (hq-support, 2026-10-09). Lines pack greedily
|
|
579
|
+
// with single newlines, so a list stays a list inside a message; only a line
|
|
580
|
+
// that is itself over the limit goes on to sentences and words.
|
|
533
581
|
const units = [];
|
|
534
582
|
for (const block of blocks) {
|
|
535
583
|
if (block.atomic || block.text.length <= limit) {
|
|
536
584
|
units.push(block.text);
|
|
537
585
|
continue;
|
|
538
586
|
}
|
|
539
|
-
let
|
|
540
|
-
const
|
|
541
|
-
if (
|
|
542
|
-
|
|
587
|
+
let group = '';
|
|
588
|
+
const flushGroup = () => {
|
|
589
|
+
if (group) units.push(group);
|
|
590
|
+
group = '';
|
|
543
591
|
};
|
|
544
|
-
for (const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
run = run ? `${run} ${word}` : word;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
if (run) units.push(run); // a single over-limit word (URL) posts whole
|
|
557
|
-
} else if (piece && `${piece} ${sentence}`.length > limit) {
|
|
558
|
-
flushPiece();
|
|
559
|
-
piece = sentence;
|
|
592
|
+
for (const rawLine of block.text.split('\n')) {
|
|
593
|
+
const line = rawLine.trimEnd(); // keep a nested item's indentation
|
|
594
|
+
if (!line.trim()) continue;
|
|
595
|
+
if (line.length > limit) {
|
|
596
|
+
flushGroup();
|
|
597
|
+
units.push(...splitLineForChat(line, limit));
|
|
598
|
+
} else if (group && `${group}\n${line}`.length > limit) {
|
|
599
|
+
flushGroup();
|
|
600
|
+
group = line;
|
|
560
601
|
} else {
|
|
561
|
-
|
|
602
|
+
group = group ? `${group}\n${line}` : line;
|
|
562
603
|
}
|
|
563
604
|
}
|
|
564
|
-
|
|
605
|
+
flushGroup();
|
|
565
606
|
}
|
|
566
607
|
|
|
567
608
|
// Pass 3: greedy packing back up to the limit.
|