ticlawk 0.1.17-dev.19 → 0.1.17-dev.20

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/README.md CHANGED
@@ -13,57 +13,42 @@ from your phone.
13
13
 
14
14
  ## Quickstart — 2 minutes
15
15
 
16
- You need a project directory on this machine where Claude Code (or another
17
- supported runtime) can run, and the Ticlawk app installed on your phone.
16
+ You need a machine with at least one supported runtime installed and the
17
+ Ticlawk app installed on your phone.
18
18
 
19
19
  ```bash
20
20
  # 1. install
21
21
  curl -fsSL https://raw.githubusercontent.com/darthjaja6/ticlawk/main/install.sh | bash
22
22
 
23
23
  # 2. connect this machine to your Ticlawk account by scanning the QR code
24
- cd /path/to/project
25
24
  ticlawk connect
26
25
  ```
27
26
 
28
- `ticlawk connect` auto-detects every supported runtime in that project
29
- directory (Codex, Claude Code, opencode, pi) and lets you pick one inside the
30
- app once pairing completes. Run `ticlawk connect` again from a different
31
- directory when you want to bind a new project.
27
+ `ticlawk connect` authorizes the current machine as a Ticlawk host. After the
28
+ host is connected, create agents in the app by choosing that host and one of
29
+ the runtimes the daemon reports as available.
32
30
 
33
31
  > ⚠️ **Single user, by design.** The connector binds to your Ticlawk account;
34
32
  > agents run with your local user's permissions and can edit files / run
35
- > commands inside the bound `--workdir`. Don't share your connector API key
36
- > and don't connect a workdir you wouldn't trust a shell to touch.
33
+ > commands on that host. Don't share your connector API key and don't connect
34
+ > a host you wouldn't trust a shell to touch.
37
35
 
38
36
  ---
39
37
 
40
- ## Resuming an existing runtime session
38
+ ## Creating Agents
41
39
 
42
- By default each new chat creates a fresh runtime session. To resume an
43
- existing Claude Code or Codex transcript, point at it explicitly:
44
-
45
- | Runtime | Required | Optional resume handle | Where to find it |
46
- |-------------|----------------|------------------------|---------------------------------------------------------------|
47
- | Claude Code | `--workdir` | `--session-id` | filename of `~/.claude/projects/**/*.jsonl` (without `.jsonl`)|
48
- | Codex | `--workdir` | `--session-id` | `payload.id` in the first `session_meta` record of `~/.codex/sessions/**/*.jsonl` |
49
- | OpenClaw | `--agent-id` | none | logical agent name (`main` by default) |
50
- | opencode | `--workdir` | `--session-id` (with `--workdir`) | `.id` from `opencode session list --format json` (run inside the project dir) |
40
+ `ticlawk connect` does not create an agent. It connects a host. The daemon
41
+ then reports runtime health for Codex, Claude Code, opencode, OpenClaw, and pi.
42
+ In the mobile app, tap `+`, choose a connected host, choose an available
43
+ runtime, and create the agent there.
51
44
 
52
45
  [OpenClaw](https://github.com/openclaw/openclaw) is an open-source local-first
53
- agent runtime; skip this row if you don't use it.
54
-
55
- [opencode](https://opencode.ai) is provider-agnostic — it doesn't carry its own
56
- account system. Before connecting through `ticlawk`, install the CLI
57
- (`npm i -g opencode-ai`) and authenticate it once against the LLM provider
58
- you want it to use (`opencode auth login`, then pick Anthropic / OpenAI /
59
- Google / OpenRouter / etc. and paste the corresponding API key). Model
60
- selection lives in opencode's own config (`opencode.json` in the project
61
- root, or user-level config) — `ticlawk` does not pass `--model`.
46
+ agent runtime; skip it if you don't use it.
62
47
 
63
- `ticlawk` spawns opencode and codex with `--dangerously-skip-permissions`-style
64
- flags so chat messages can drive shell commands and file edits in the bound
65
- workdir without further confirmation prompts. Only connect a workdir you
66
- trust to a chat you control.
48
+ [opencode](https://opencode.ai) is provider-agnostic. Before creating an
49
+ opencode-backed agent through Ticlawk, install the CLI (`npm i -g opencode-ai`)
50
+ and authenticate it once against the LLM provider you want it to use
51
+ (`opencode auth login`).
67
52
 
68
53
  ---
69
54
 
@@ -74,11 +59,10 @@ After setup:
74
59
  - Keep `ticlawk` running as a user service. If Linux prints an
75
60
  `Action required` message during install or connect, run the command it
76
61
  shows so the daemon keeps running after logout and reboot.
77
- - Bind to a new project: run `ticlawk connect` from the new directory.
78
- - Resume an existing transcript: add `--session-id` (Claude Code / Codex /
79
- opencode) or `--agent-id` (OpenClaw) to the `connect` call.
80
- - If replies stop because the local runtime session ended, run `connect`
81
- again or reset the session from the app.
62
+ - Connect another host: run `ticlawk connect` on that machine.
63
+ - Create a new agent from the app by choosing a connected host and runtime.
64
+ - If replies stop because the local runtime session ended, reset the agent
65
+ session from the app.
82
66
  - `ticlawk health` is the first thing to check when something feels off.
83
67
 
84
68
  ---
@@ -201,7 +185,6 @@ Usage:
201
185
  ticlawk config
202
186
  ticlawk config get <streaming...|runtimes.claude_code.path|runtimes.codex.path|runtimes.opencode.path|runtimes.pi.path|ticlawk.connector-api-key|ticlawk.api-url|ticlawk.connector-ws-url>
203
187
  ticlawk config set <streaming...|runtimes.claude_code.path|runtimes.codex.path|runtimes.opencode.path|runtimes.pi.path|ticlawk.connector-api-key|ticlawk.api-url|ticlawk.connector-ws-url> <value>
204
- ticlawk auth --code <6-digit-code> [--api-url <url>]
205
188
  ticlawk connect
206
189
  ticlawk profile list
207
190
  ticlawk profile current
@@ -221,8 +204,7 @@ Agent CLI (run inside an agent runtime; requires TICLAWK_RUNTIME_AGENT_ID):
221
204
  ticlawk server info [--refresh]
222
205
 
223
206
  Commands:
224
- auth store the ticlawk app pairing code locally
225
- connect connect ticlawk to a local runtime
207
+ connect connect this host to Ticlawk
226
208
  profile list or switch saved local identities
227
209
  message send/read chat messages (agent CLI surface)
228
210
  task claim/update/list tasks (agent CLI surface)
@@ -245,7 +227,6 @@ Config examples:
245
227
 
246
228
  Examples:
247
229
  ticlawk connect
248
- ticlawk auth --code <6-digit-code>
249
230
  ```
250
231
  <!-- usage:end -->
251
232
 
@@ -254,7 +235,7 @@ Examples:
254
235
  ## Security model
255
236
 
256
237
  - **Agents can run code on your machine.** Whatever Claude Code / Codex /
257
- OpenClaw / opencode / pi can do in the bound workdir, the Ticlawk app can
238
+ OpenClaw / opencode / pi can do on the connected host, the Ticlawk app can
258
239
  ask it to do. Treat the chat as you'd treat a shell.
259
240
  - **Single user.** The connector binds to one Ticlawk account. Don't share
260
241
  your connector API key.
package/bin/ticlawk.mjs CHANGED
@@ -18,7 +18,6 @@ import {
18
18
  getActiveProfile,
19
19
  listProfiles,
20
20
  } from '../src/core/profiles.mjs';
21
- import { runAdapterAuth } from '../src/core/adapter-registry.mjs';
22
21
  import { runTiclawkConnect } from '../src/core/ticlawk-control.mjs';
23
22
  import {
24
23
  RUNTIME_DEFINITIONS,
@@ -81,7 +80,6 @@ Usage:
81
80
  ticlawk config
82
81
  ticlawk config get <${configKeys}>
83
82
  ticlawk config set <${configKeys}> <value>
84
- ticlawk auth --code <6-digit-code> [--api-url <url>]
85
83
  ticlawk connect
86
84
  ticlawk profile list
87
85
  ticlawk profile current
@@ -101,8 +99,7 @@ Agent CLI (run inside an agent runtime; requires TICLAWK_RUNTIME_AGENT_ID):
101
99
  ticlawk server info [--refresh]
102
100
 
103
101
  Commands:
104
- auth store the ticlawk app pairing code locally
105
- connect connect ticlawk to a local runtime
102
+ connect connect this host to Ticlawk
106
103
  profile list or switch saved local identities
107
104
  message send/read chat messages (agent CLI surface)
108
105
  task claim/update/list tasks (agent CLI surface)
@@ -122,30 +119,15 @@ ${getRuntimeConfigExamples()}
122
119
 
123
120
  Examples:
124
121
  ticlawk connect
125
- ticlawk auth --code <6-digit-code>
126
- `;
127
- }
128
-
129
- function getAuthHelp() {
130
- return `ticlawk auth --code <6-digit-code> [--api-url <url>]
131
-
132
- Store the ticlawk app pairing code locally. The daemon picks it up on the
133
- next start and exchanges it for an API key.
134
-
135
- Options:
136
- --code <code> 6-digit setup code from the ticlawk app
137
- --api-url <url> optional ticlawk API base URL override
138
122
  `;
139
123
  }
140
124
 
141
125
  function getConnectHelp() {
142
126
  return `ticlawk connect
143
127
 
144
- Use \`connect\` to connect the selected adapter to a local runtime.
145
- For ticlawk, connect starts the QR pairing flow when no local credential exists.
146
- Run \`ticlawk connect\` from a project directory to auto-detect
147
- Codex, Claude Code, opencode, and pi, then choose the runtime in ticlawk.
148
- No pairing QR code is shown when no local agent harness is detected.
128
+ Use \`connect\` to authorize this machine as a Ticlawk host.
129
+ After the host is connected, create agents from the app by choosing this host
130
+ and one of the detected local runtimes.
149
131
 
150
132
  Examples:
151
133
  ticlawk connect
@@ -162,10 +144,6 @@ function printHelp(helpPath, args = {}) {
162
144
  printUsage();
163
145
  return;
164
146
  }
165
- if (head === 'auth') {
166
- console.log(getAuthHelp());
167
- return;
168
- }
169
147
  if (head === 'connect') {
170
148
  console.log(getConnectHelp());
171
149
  return;
@@ -242,7 +220,7 @@ function formatSecretConfigValue(configKey, value) {
242
220
  }
243
221
 
244
222
  function buildConnectPayload(args) {
245
- const legacyArgs = [
223
+ const rejectedArgs = [
246
224
  'adapter',
247
225
  'type',
248
226
  'session-id',
@@ -252,21 +230,19 @@ function buildConnectPayload(args) {
252
230
  'agent-id',
253
231
  'runtime-path',
254
232
  'code',
233
+ 'name',
234
+ 'switch-user',
255
235
  ].filter((key) => args[key] !== undefined);
256
236
  const positionalRuntime = args._?.[1];
257
- if (positionalRuntime || legacyArgs.length > 0) {
237
+ if (positionalRuntime || rejectedArgs.length > 0) {
258
238
  const suffix = positionalRuntime ? ` ${positionalRuntime}` : '';
259
239
  console.error(`ticlawk connect${suffix} no longer accepts explicit runtime, adapter, session, or workdir arguments.`);
260
- console.error('Run `ticlawk connect` from the directory you want to offer to local agent harnesses.');
261
- console.error('Ticlawk will detect Codex, Claude Code, opencode, and pi locally, then let you choose in the app.');
240
+ console.error('Run `ticlawk connect` with no runtime arguments to authorize this host.');
241
+ console.error('Create agents from the app after the host is connected.');
262
242
  process.exit(1);
263
243
  }
264
244
  return {
265
245
  adapter: 'ticlawk',
266
- autoRuntime: true,
267
- switchUser: Boolean(args['switch-user']),
268
- workdir: process.cwd(),
269
- ...(args.name ? { name: args.name } : {}),
270
246
  };
271
247
  }
272
248
 
@@ -637,18 +613,6 @@ async function main() {
637
613
  return;
638
614
  }
639
615
 
640
- if (command === 'auth') {
641
- if (args.help || args.h) {
642
- printHelp(['auth'], args);
643
- return;
644
- }
645
- const adapterArgv = rawArgv.slice(1);
646
- const res = await runAdapterAuth('ticlawk', adapterArgv);
647
- printCommandResult(res);
648
- process.exitCode = res.statusCode >= 400 ? 1 : 0;
649
- return;
650
- }
651
-
652
616
  if (command === 'connect') {
653
617
  if (args.help || args.h) {
654
618
  console.log(getConnectHelp());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ticlawk",
3
- "version": "0.1.17-dev.19",
3
+ "version": "0.1.17-dev.20",
4
4
  "description": "Local connector that links agent harnesses (Claude Code, Codex, OpenClaw, opencode, Pi) to the Ticlawk mobile app.",
5
5
  "type": "module",
6
6
  "main": "ticlawk.mjs",
@@ -15,6 +15,7 @@
15
15
  "assets/",
16
16
  "bin/",
17
17
  "scripts/postinstall.mjs",
18
+ "scripts/publish-dev.sh",
18
19
  "src/",
19
20
  "cc-watcher.mjs",
20
21
  "ticlawk.mjs",
@@ -33,7 +34,8 @@
33
34
  "health": "node ./bin/ticlawk.mjs health",
34
35
  "dev": "echo 'Configure ~/.ticlawk/.config, then: npm run start'",
35
36
  "generate:usage-docs": "node scripts/generate-usage-docs.mjs",
36
- "verify:public-package": "bash scripts/verify-ticlawk-public.sh"
37
+ "verify:public-package": "bash scripts/verify-ticlawk-public.sh",
38
+ "publish:dev": "bash scripts/publish-dev.sh"
37
39
  },
38
40
  "repository": {
39
41
  "type": "git",
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ cd "$ROOT_DIR"
6
+
7
+ EXPECTED_VERSION=""
8
+ DRY_RUN=false
9
+
10
+ while [[ $# -gt 0 ]]; do
11
+ case "$1" in
12
+ --expected-version)
13
+ EXPECTED_VERSION="${2:-}"
14
+ if [[ -z "$EXPECTED_VERSION" ]]; then
15
+ echo "--expected-version requires a value" >&2
16
+ exit 2
17
+ fi
18
+ shift 2
19
+ ;;
20
+ --dry-run)
21
+ DRY_RUN=true
22
+ shift
23
+ ;;
24
+ *)
25
+ echo "unknown argument: $1" >&2
26
+ echo "usage: npm run publish:dev -- [--expected-version X.Y.Z-dev.N] [--dry-run]" >&2
27
+ exit 2
28
+ ;;
29
+ esac
30
+ done
31
+
32
+ VERSION="$(node -p "JSON.parse(require('fs').readFileSync('package.json', 'utf8')).version")"
33
+
34
+ if [[ -n "$EXPECTED_VERSION" && "$EXPECTED_VERSION" != "$VERSION" ]]; then
35
+ echo "expected version ($EXPECTED_VERSION) does not match cli/package.json ($VERSION)" >&2
36
+ exit 1
37
+ fi
38
+
39
+ if [[ "$VERSION" != *-* ]]; then
40
+ echo "refusing to publish stable version $VERSION with the dev script" >&2
41
+ exit 1
42
+ fi
43
+
44
+ NPM_ARGS=(--registry https://registry.npmjs.org/)
45
+ TMP_NPMRC=""
46
+ if [[ -n "${NPM_TOKEN:-}" ]]; then
47
+ TMP_NPMRC="$(mktemp)"
48
+ chmod 0600 "$TMP_NPMRC"
49
+ printf '//registry.npmjs.org/:_authToken=%s\n' "$NPM_TOKEN" > "$TMP_NPMRC"
50
+ NPM_ARGS+=(--userconfig "$TMP_NPMRC")
51
+ fi
52
+
53
+ cleanup() {
54
+ if [[ -n "$TMP_NPMRC" ]]; then
55
+ rm -f "$TMP_NPMRC"
56
+ fi
57
+ }
58
+ trap cleanup EXIT
59
+
60
+ if npm "${NPM_ARGS[@]}" view "ticlawk@$VERSION" version >/dev/null 2>&1; then
61
+ echo "ticlawk@$VERSION is already published" >&2
62
+ exit 1
63
+ fi
64
+
65
+ if ! npm "${NPM_ARGS[@]}" whoami >/dev/null; then
66
+ echo "npm auth failed; run npm login or set a valid NPM_TOKEN with publish access to ticlawk" >&2
67
+ exit 1
68
+ fi
69
+
70
+ npm run verify:public-package
71
+
72
+ PUBLISH_ARGS=(publish --access public --tag dev)
73
+ if [[ "$DRY_RUN" == "true" ]]; then
74
+ PUBLISH_ARGS+=(--dry-run)
75
+ fi
76
+
77
+ npm "${NPM_ARGS[@]}" "${PUBLISH_ARGS[@]}"
@@ -552,34 +552,6 @@ export async function postEvent({ agent, agent_id, runtime_host_id, session_id,
552
552
  return queued;
553
553
  }
554
554
 
555
- // ── Pair (no auth needed) ──
556
-
557
- export async function pair(payload) {
558
- const url = `${getApiUrl()}/dispatch`;
559
- const res = await fetch(url, {
560
- method: 'POST',
561
- headers: { 'Content-Type': 'application/json' },
562
- body: JSON.stringify({ action: 'pair', ...payload }),
563
- signal: AbortSignal.timeout(15000),
564
- });
565
- const body = await res.json().catch(() => ({}));
566
- return {
567
- statusCode: res.status,
568
- ...body,
569
- };
570
- }
571
-
572
- export async function pairPreview(payload) {
573
- const url = `${getApiUrl()}/dispatch`;
574
- const res = await fetch(url, {
575
- method: 'POST',
576
- headers: { 'Content-Type': 'application/json' },
577
- body: JSON.stringify({ action: 'pair-preview', ...payload }),
578
- signal: AbortSignal.timeout(15000),
579
- });
580
- return res.json();
581
- }
582
-
583
555
  async function pairingRequest(payload, timeout = 15000) {
584
556
  const url = `${getApiUrl()}/api/agent-pairings`;
585
557
  const res = await fetch(url, {
@@ -13,9 +13,7 @@ export function persistApiCredential(apiKey) {
13
13
 
14
14
  persistConfig({
15
15
  [TICLAWK_CONNECTOR_API_KEY]: apiKey,
16
- TICLAWK_SETUP_CODE: '',
17
16
  });
18
17
  process.env[TICLAWK_CONNECTOR_API_KEY] = apiKey;
19
- delete process.env.TICLAWK_SETUP_CODE;
20
18
  console.log(`[connect] saved ${TICLAWK_CONNECTOR_API_KEY} to ${AF_CONFIG_PATH}`);
21
19
  }
@@ -1,11 +1,9 @@
1
- import { parseOptionArgs } from '../../core/argv.mjs';
2
1
  import { createHash, randomBytes } from 'node:crypto';
3
2
  import { createRequire } from 'node:module';
4
- import { basename } from 'node:path';
5
3
  import { loadPersistentConfig, persistConfig, TICLAWK_CONNECTOR_API_KEY, TICLAWK_CONNECTOR_WS_URL } from '../../core/config.mjs';
6
4
  import { belongsToRuntimeHost, getBindingRuntimeHostId, getHostId, getHostLabel } from '../../core/host-id.mjs';
7
5
  import { debugError, debugLog } from '../../core/logger.mjs';
8
- import { getActiveProfile, ensureLegacyProfile, readProfileMeta, saveAndActivateProfile } from '../../core/profiles.mjs';
6
+ import { getActiveProfile, saveAndActivateProfile } from '../../core/profiles.mjs';
9
7
  import { isTerminalRuntimeFailure } from '../../core/runtime-support.mjs';
10
8
  import { clearUpdateRequiredState, readUpdateState, setUpdateRequiredState } from '../../core/update-state.mjs';
11
9
  import { isManagedInstall, startDetachedSelfUpdate } from '../../core/update.mjs';
@@ -71,9 +69,6 @@ function normalizeInboundMediaAssets(msg) {
71
69
  }
72
70
 
73
71
  export function normalizeInboundMessage(msg) {
74
- // Claimed delivery rows carry the recipient agent id; legacy messages
75
- // (history sync, manual inserts) may still use plain agent_id. Prefer
76
- // the new field but fall back so the same normalizer works for both.
77
72
  const recipientAgentId = msg.recipient_agent_id || '';
78
73
  const messageId = msg.id || msg.message_id || null;
79
74
  const deliveryId = msg.delivery_id || null;
@@ -123,16 +118,12 @@ function getRuntimeHostLabelFromPayload(payload) {
123
118
 
124
119
  function getAgentIdFromPayload(payload) {
125
120
  // claim_pending_deliveries is the canonical source and returns
126
- // `recipient_agent_id`. The historical fallbacks (`agent_id`,
127
- // `agentId`) were left in by an earlier partial cleanup but they
128
- // never fire against the current schema — dead branches.
121
+ // `recipient_agent_id`.
129
122
  return String(payload?.recipient_agent_id || '').trim();
130
123
  }
131
124
 
132
125
  // Whitelist of meta keys runtimes actually consume. Anything else in
133
- // the source row's meta blob is dropped on the floor so stale fields
134
- // (like the post-Y2 workdir/cwd/projectDir leftovers) can't sneak
135
- // back into runtimeMeta and confuse downstream code.
126
+ // the source row's meta blob is dropped before reaching runtimeMeta.
136
127
  const RUNTIME_META_KEYS = [
137
128
  'sessionId',
138
129
  'path',
@@ -225,24 +216,6 @@ function maskIdentity(identity = {}) {
225
216
  };
226
217
  }
227
218
 
228
- function formatIdentityLines(identity = {}) {
229
- const normalized = maskIdentity(identity);
230
- return [
231
- ` user: ${normalized.userId || 'unknown'}`,
232
- normalized.emailMasked ? ` email: ${normalized.emailMasked}` : null,
233
- normalized.phoneMasked ? ` phone: ${normalized.phoneMasked}` : null,
234
- ].filter(Boolean);
235
- }
236
-
237
- function formatAffectedAgents(agents = []) {
238
- if (!agents.length) return [' - none found for this host'];
239
- return agents.map((agent) => {
240
- const display = agent.display_name || agent.name || agent.id;
241
- const runtime = agent.service_type || 'unknown';
242
- return ` - ${display} (${runtime}) ${agent.id}`;
243
- });
244
- }
245
-
246
219
  function sha256Hex(value) {
247
220
  return createHash('sha256').update(String(value)).digest('hex');
248
221
  }
@@ -251,63 +224,6 @@ function makeClientSecret() {
251
224
  return randomBytes(32).toString('base64url');
252
225
  }
253
226
 
254
- function getRuntimeWorkdir(runtimeMeta = {}) {
255
- return String(runtimeMeta.workdir || runtimeMeta.cwd || runtimeMeta.projectDir || '').trim();
256
- }
257
-
258
- function runtimeLabel(runtime) {
259
- if (runtime === 'claude_code') return 'Claude Code';
260
- if (runtime === 'opencode') return 'OpenCode';
261
- if (runtime === 'openclaw') return 'OpenClaw';
262
- if (runtime === 'codex') return 'Codex';
263
- if (runtime === 'pi') return 'Pi';
264
- return runtime || 'Agent';
265
- }
266
-
267
- function printDetectedRuntimeOptions(runtimeOptions = []) {
268
- console.log('Detected agent harness:');
269
- if (!runtimeOptions.length) {
270
- console.log(' none');
271
- return;
272
- }
273
- for (const option of runtimeOptions) {
274
- const label = option.runtime_label || runtimeLabel(option.runtime);
275
- const workdir = option.workdir ? ` (${option.workdir})` : '';
276
- console.log(` - ${label}${workdir}`);
277
- }
278
- }
279
-
280
- async function buildAutoRuntimeOptions(ctx, payload = {}) {
281
- const workdir = getRuntimeWorkdir(payload) || process.cwd();
282
- const candidates = ['codex', 'claude_code', 'opencode', 'pi'];
283
- const options = [];
284
- for (const serviceType of candidates) {
285
- try {
286
- const resolved = await ctx.resolveRuntimeBinding({
287
- ...payload,
288
- serviceType,
289
- workdir,
290
- });
291
- const runtimeMeta = resolved.runtimeMeta || {};
292
- const optionWorkdir = getRuntimeWorkdir(runtimeMeta) || workdir;
293
- options.push({
294
- runtime: resolved.runtime,
295
- runtime_label: runtimeLabel(resolved.runtime),
296
- display_name: resolved.displayName || basename(optionWorkdir) || runtimeLabel(resolved.runtime),
297
- workdir: optionWorkdir,
298
- binding_key: optionWorkdir,
299
- runtime_meta: runtimeMeta,
300
- });
301
- } catch (err) {
302
- debugLog('ticlawk-pairing', 'auto-runtime.skip', {
303
- runtime: serviceType,
304
- reason: err?.message || String(err),
305
- });
306
- }
307
- }
308
- return options;
309
- }
310
-
311
227
  function printPairingChallenge(session) {
312
228
  console.log();
313
229
  console.log('Open Ticlawk and scan the QR code below, or enter the pairing code.');
@@ -372,6 +288,7 @@ export function createTiclawkAdapter(ctx) {
372
288
  let lastBindingsWakeAt = 0;
373
289
  let updateRequired = null;
374
290
  let lastUpdateRequiredLogAt = 0;
291
+ let startupSyncing = false;
375
292
 
376
293
  function clearDebounce(timer) {
377
294
  if (timer) clearTimeout(timer);
@@ -851,10 +768,11 @@ export function createTiclawkAdapter(ctx) {
851
768
  function handleWakeEvent(event) {
852
769
  wakeState.lastEventAt = new Date().toISOString();
853
770
  if (event?.type === 'hello') {
771
+ void reportHostCapabilitiesNow();
772
+ if (startupSyncing) return;
854
773
  void refreshBindings('wake.hello')
855
774
  .then(() => requestDrain('wake.hello'))
856
775
  .catch(() => {});
857
- void reportHostCapabilitiesNow();
858
776
  return;
859
777
  }
860
778
  if (event?.type === 'jobs.available') {
@@ -907,9 +825,21 @@ export function createTiclawkAdapter(ctx) {
907
825
  });
908
826
  }
909
827
 
828
+ function getConnectorWakeUrlForHost() {
829
+ const raw = api.getConnectorWsUrl();
830
+ try {
831
+ const url = new URL(raw);
832
+ url.searchParams.set('runtime_host_id', hostId);
833
+ return url.toString();
834
+ } catch {
835
+ const separator = raw.includes('?') ? '&' : '?';
836
+ return `${raw}${separator}runtime_host_id=${encodeURIComponent(hostId)}`;
837
+ }
838
+ }
839
+
910
840
  function connectWakeSocket() {
911
841
  connectorSocket = new TiclawkWakeClient({
912
- getUrl: api.getConnectorWsUrl,
842
+ getUrl: getConnectorWakeUrlForHost,
913
843
  getApiKey: api.getApiKey,
914
844
  onEvent: handleWakeEvent,
915
845
  onStatus: handleWakeStatus,
@@ -937,7 +867,7 @@ export function createTiclawkAdapter(ctx) {
937
867
  bindingAuditTimer.unref?.();
938
868
  }
939
869
 
940
- async function finishPairing(resolved, pairData) {
870
+ async function finishHostPairing(pairData) {
941
871
  const apiKey = pairData.connector_api_key || pairData.apiKey || pairData.api_key;
942
872
  persistApiCredential(apiKey);
943
873
  if (pairData.connector_ws_url) {
@@ -956,61 +886,26 @@ export function createTiclawkAdapter(ctx) {
956
886
  restartWakeSocket('connect.paired');
957
887
  }
958
888
 
959
- const bindingId = pairData.agent_id || pairData.agentId;
960
- if (!bindingId) {
961
- throw new Error('pairing did not return agent_id');
962
- }
963
- const runtimeMeta = pairData.runtime_meta || resolved.runtimeMeta;
964
- const binding = await ctx.upsertBinding({
965
- id: bindingId,
966
- adapter: 'ticlawk',
967
- targetKey: bindingId,
968
- targetMeta: {
969
- agentId: bindingId,
970
- runtime_host_id: hostId,
971
- binding_key: pairData.binding_key || null,
972
- },
973
- runtime_host_id: hostId,
974
- runtime_host_label: hostLabel,
975
- runtime: resolved.runtime,
976
- runtimeMeta,
977
- displayName: resolved.displayName,
978
- status: 'connected',
979
- });
889
+ await reportHostCapabilitiesNow();
980
890
  return {
981
891
  statusCode: 200,
982
892
  body: {
983
893
  ok: true,
984
- agentId: binding.id,
985
- serviceType: resolved.runtime,
986
- name: binding.displayName,
987
- bindingKey: pairData.binding_key || null,
894
+ hostId,
895
+ hostLabel,
988
896
  user: pairedIdentity,
989
897
  },
990
898
  };
991
899
  }
992
900
 
993
- async function connectWithQrPairing(payload) {
994
- const autoRuntime = Boolean(payload?.autoRuntime);
995
- const runtimeOptions = autoRuntime ? await buildAutoRuntimeOptions(ctx, payload) : [];
996
- if (autoRuntime) {
997
- printDetectedRuntimeOptions(runtimeOptions);
998
- if (runtimeOptions.length === 0) {
999
- return connectError(400, 'No supported local agent harness detected in this terminal. Install or sign in to Codex, Claude Code, OpenCode, or pi, then run `ticlawk connect` again.');
1000
- }
1001
- }
1002
- const resolved = autoRuntime ? null : await ctx.resolveRuntimeBinding(payload);
1003
- const runtimeMeta = resolved?.runtimeMeta || {};
1004
- const workdir = getRuntimeWorkdir(runtimeMeta) || getRuntimeWorkdir(payload) || process.cwd();
901
+ async function connectWithQrPairing() {
1005
902
  const clientSecret = makeClientSecret();
1006
903
  const created = await api.createPairingSession({
1007
904
  client: 'ticlawk',
1008
905
  client_version: api.getTiclawkVersion(),
1009
906
  host_id: hostId,
1010
907
  host_label: hostLabel,
1011
- ...(autoRuntime ? { runtime_options: runtimeOptions } : { runtime: resolved.runtime }),
1012
- workdir,
1013
- display_name: resolved?.displayName || basename(workdir) || 'Agent',
908
+ scope: 'host',
1014
909
  challenge_hash: sha256Hex(clientSecret),
1015
910
  });
1016
911
  if (!created?.ok) {
@@ -1031,20 +926,9 @@ export function createTiclawkAdapter(ctx) {
1031
926
  pollAfterMs: Number(created.poll_after_ms || 1500),
1032
927
  expiresAt: created.expires_at,
1033
928
  });
1034
- const approvedRuntime = approved.runtime || approved.selected_runtime;
1035
- const approvedOption = runtimeOptions.find((option) => option.runtime === approvedRuntime) || null;
1036
- const finalResolved = resolved || {
1037
- runtime: approvedRuntime || approvedOption?.runtime,
1038
- displayName: approved.display_name || approvedOption?.display_name || runtimeLabel(approvedRuntime),
1039
- runtimeMeta: approved.runtime_meta || approvedOption?.runtime_meta || {},
1040
- };
1041
- if (!finalResolved.runtime) {
1042
- throw new Error('ticlawk pairing did not return a selected runtime');
1043
- }
1044
- return await finishPairing.call(this, finalResolved, {
929
+ return await finishHostPairing({
1045
930
  ...created,
1046
931
  ...approved,
1047
- binding_key: approved.binding_key || created.binding_key || null,
1048
932
  });
1049
933
  } catch (err) {
1050
934
  await api.cancelPairingSession({
@@ -1063,9 +947,14 @@ export function createTiclawkAdapter(ctx) {
1063
947
  // mid-claim, the row stays `claimed` forever. lease_expires_at
1064
948
  // was dropped in X1; no cron has replaced it. Rare in practice;
1065
949
  // when it happens the fix is a one-row UPDATE in supabase.
1066
- await refreshBindings('startup');
1067
- await requestDrain('startup');
950
+ startupSyncing = true;
1068
951
  connectWakeSocket();
952
+ try {
953
+ await refreshBindings('startup');
954
+ await requestDrain('startup');
955
+ } finally {
956
+ startupSyncing = false;
957
+ }
1069
958
  startAuditTimers();
1070
959
  },
1071
960
 
@@ -1102,131 +991,9 @@ export function createTiclawkAdapter(ctx) {
1102
991
  },
1103
992
 
1104
993
  async connect(payload) {
1105
- const config = loadPersistentConfig();
1106
- if (!payload?.__legacyConnect) {
1107
- try {
1108
- return await connectWithQrPairing.call(this, {
1109
- ...payload,
1110
- autoRuntime: true,
1111
- });
1112
- } catch (err) {
1113
- return connectError(err?.status || 500, err.message);
1114
- }
1115
- }
1116
-
1117
- // Archived setup-code connect path. It is intentionally not reachable
1118
- // from the public CLI; keep it here only until the old flow is deleted.
1119
- const connectCode = String(payload?.code || config.TICLAWK_SETUP_CODE || '').trim();
1120
- if (!connectCode) {
1121
- return connectError(400, 'legacy ticlawk setup-code connect requires code');
1122
- }
1123
-
1124
994
  try {
1125
- const resolved = await ctx.resolveRuntimeBinding(payload);
1126
- const runtimeMeta = resolved.runtimeMeta;
1127
- let currentIdentity = null;
1128
- const activeProfile = getActiveProfile();
1129
- if (activeProfile?.adapter === 'ticlawk') {
1130
- currentIdentity = readProfileMeta(activeProfile.adapter, activeProfile.userId) || {
1131
- userId: activeProfile.userId,
1132
- };
1133
- } else if (config[TICLAWK_CONNECTOR_API_KEY]) {
1134
- try {
1135
- const me = await api.getMe();
1136
- if (me?.userId || me?.user_id) {
1137
- currentIdentity = maskIdentity(me);
1138
- ensureLegacyProfile({
1139
- adapter: 'ticlawk',
1140
- userId: currentIdentity.userId,
1141
- meta: currentIdentity,
1142
- });
1143
- }
1144
- } catch {}
1145
- }
1146
-
1147
- let previewIdentity = null;
1148
- try {
1149
- const preview = await api.pairPreview({ code: connectCode });
1150
- if (preview?.ok) {
1151
- previewIdentity = maskIdentity(preview);
1152
- } else if (preview?.userId || preview?.user_id) {
1153
- previewIdentity = maskIdentity(preview);
1154
- }
1155
- } catch {}
1156
-
1157
- if (currentIdentity?.userId && !previewIdentity?.userId && !payload.switchUser) {
1158
- return {
1159
- statusCode: 409,
1160
- body: {
1161
- ok: false,
1162
- code: 'ticlawk_pairing_user_unverified',
1163
- currentUser: currentIdentity,
1164
- error: [
1165
- 'This ticlawk home is already paired to ticlawk user:',
1166
- ...formatIdentityLines(currentIdentity),
1167
- '',
1168
- 'Could not verify which ticlawk user owns the new pairing code.',
1169
- 'Refusing to switch users because existing agents may stop processing messages.',
1170
- '',
1171
- 'If you want to switch user, please rerun this command with --switch-user.',
1172
- ].join('\n'),
1173
- },
1174
- };
1175
- }
1176
-
1177
- if (
1178
- currentIdentity?.userId
1179
- && previewIdentity?.userId
1180
- && currentIdentity.userId !== previewIdentity.userId
1181
- && !payload.switchUser
1182
- ) {
1183
- let affectedAgents = [];
1184
- try {
1185
- affectedAgents = await api.getAgents({ hostId });
1186
- } catch {}
1187
- const message = [
1188
- 'This ticlawk home is already paired to ticlawk user:',
1189
- ...formatIdentityLines(currentIdentity),
1190
- '',
1191
- 'The pairing code belongs to a different ticlawk user:',
1192
- ...formatIdentityLines(previewIdentity),
1193
- '',
1194
- 'Refusing to switch users because these agents are currently bound to this host:',
1195
- ...formatAffectedAgents(affectedAgents),
1196
- '',
1197
- 'Switching would stop this daemon from processing messages for those agents.',
1198
- '',
1199
- 'If you want to switch user, please rerun this command with --switch-user.',
1200
- ].join('\n');
1201
- return {
1202
- statusCode: 409,
1203
- body: {
1204
- ok: false,
1205
- error: message,
1206
- code: 'ticlawk_user_mismatch',
1207
- currentUser: currentIdentity,
1208
- pairingUser: previewIdentity,
1209
- affectedAgents,
1210
- },
1211
- };
1212
- }
1213
-
1214
- const pairData = await api.pair({
1215
- code: connectCode,
1216
- name: resolved.displayName,
1217
- serviceType: resolved.runtime,
1218
- runtimeMeta,
1219
- runtime_host_id: hostId,
1220
- runtime_host_label: hostLabel,
1221
- });
1222
- if (!pairData?.ok) {
1223
- return { statusCode: pairData?.statusCode || 401, body: pairData };
1224
- }
1225
- return finishPairing.call(this, resolved, {
1226
- ...pairData,
1227
- agent_id: pairData.agentId,
1228
- connector_api_key: pairData.apiKey,
1229
- });
995
+ void payload;
996
+ return await connectWithQrPairing();
1230
997
  } catch (err) {
1231
998
  return connectError(err?.status || 500, err.message);
1232
999
  }
@@ -1290,52 +1057,3 @@ export function createTiclawkAdapter(ctx) {
1290
1057
  },
1291
1058
  };
1292
1059
  }
1293
-
1294
- export function getTiclawkAuthHelp() {
1295
- return `ticlawk auth ticlawk --code <6-digit-code> [--api-url <url>]
1296
-
1297
- Options:
1298
- --code <code> 6-digit setup code from the ticlawk app
1299
- --api-url <url> optional ticlawk API base URL override
1300
- `;
1301
- }
1302
-
1303
- export async function runTiclawkAuth(rawArgs) {
1304
- const args = parseOptionArgs(rawArgs);
1305
- if (args.help || args.h) {
1306
- return {
1307
- statusCode: 200,
1308
- body: {
1309
- ok: true,
1310
- help: getTiclawkAuthHelp(),
1311
- },
1312
- };
1313
- }
1314
- const code = String(args.code || '').trim();
1315
- if (!code) {
1316
- return {
1317
- statusCode: 400,
1318
- body: {
1319
- ok: false,
1320
- error: 'ticlawk auth requires --code',
1321
- },
1322
- };
1323
- }
1324
- const updates = {
1325
- TICLAWK_SETUP_CODE: code,
1326
- };
1327
- const apiUrl = String(args['api-url'] || '').trim();
1328
- if (apiUrl) {
1329
- updates.TICLAWK_API_URL = apiUrl;
1330
- }
1331
- persistConfig(updates);
1332
- return {
1333
- statusCode: 200,
1334
- body: {
1335
- ok: true,
1336
- adapter: 'ticlawk',
1337
- setupCode: 'set',
1338
- apiUrl: apiUrl || undefined,
1339
- },
1340
- };
1341
- }
@@ -1,8 +1,4 @@
1
- import {
2
- createTiclawkAdapter,
3
- getTiclawkAuthHelp,
4
- runTiclawkAuth,
5
- } from '../adapters/ticlawk/index.mjs';
1
+ import { createTiclawkAdapter } from '../adapters/ticlawk/index.mjs';
6
2
 
7
3
  /**
8
4
  * Ticlawk has a single adapter: ticlawk itself. The registry shape is kept
@@ -18,17 +14,3 @@ export function createAdapter(adapterId, ctx) {
18
14
  }
19
15
  return createTiclawkAdapter(ctx);
20
16
  }
21
-
22
- export function getAdapterAuthHelp(adapterId) {
23
- if (adapterId !== ADAPTER_ID) {
24
- throw new Error(`unsupported adapter: ${adapterId}`);
25
- }
26
- return getTiclawkAuthHelp();
27
- }
28
-
29
- export async function runAdapterAuth(adapterId, rawArgs) {
30
- if (adapterId !== ADAPTER_ID) {
31
- throw new Error(`unsupported adapter: ${adapterId}`);
32
- }
33
- return runTiclawkAuth(rawArgs);
34
- }
@@ -92,7 +92,6 @@ export function saveProfile({ adapter, userId, config = {}, meta = {} }) {
92
92
  mkdirSync(dir, { recursive: true });
93
93
  const currentConfig = readProfileConfig(adapter, userId);
94
94
  const nextConfig = { ...currentConfig, ...config };
95
- delete nextConfig.TICLAWK_SETUP_CODE;
96
95
  writeDotenvAtomic(getProfileConfigPath(adapter, userId), nextConfig);
97
96
 
98
97
  const currentMeta = readProfileMeta(adapter, userId) || {};
@@ -15,7 +15,6 @@
15
15
  const STRIPPED_KEYS = new Set([
16
16
  'TICLAWK_CONNECTOR_API_KEY',
17
17
  'TICLAWK_CONNECTOR_WS_URL',
18
- 'TICLAWK_SETUP_CODE',
19
18
  ]);
20
19
 
21
20
  export function buildRuntimeEnv(extra = {}) {
package/ticlawk.mjs CHANGED
@@ -266,20 +266,17 @@ export async function startTiclawk() {
266
266
  baseRuntimeCtx = createBaseRuntimeCtx(runtimes, persistBinding, (binding) => syncBinding(binding));
267
267
 
268
268
  printBanner(adapter);
269
- if (typeof adapter.refreshBindings === 'function') {
270
- await adapter.refreshBindings();
271
- }
272
269
  registerRuntimeHandlers(runtimeList, baseRuntimeCtx, adapter);
273
- await replayBindings(runtimes, adapter);
274
270
  startLocalHttpServer({
275
271
  port: HTTP_PORT,
276
272
  adapter,
277
273
  ctx: { listBindings, getBinding },
278
274
  });
279
275
  startReminderTicker();
276
+ await adapter.start();
277
+ await replayBindings(runtimes, adapter);
280
278
  await recoverAllRuntimes(runtimeList, adapter);
281
279
  await reconcileBindingsAfterRestart(runtimes, adapter);
282
- await adapter.start();
283
280
  }
284
281
 
285
282
  if (import.meta.url === `file://${process.argv[1]}`) {