thinkpool-pair 0.7.319 → 0.7.320
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 +6 -6
- package/account.mjs +11 -10
- package/bridge.mjs +6 -5
- package/command-guidance.mjs +8 -0
- package/package.json +2 -1
- package/provider.mjs +5 -4
- package/serve-consent.mjs +3 -1
- package/service.mjs +3 -2
package/README.md
CHANGED
|
@@ -31,14 +31,14 @@ The bridge self-heals at three levels — all cross-platform (macOS / Linux / Wi
|
|
|
31
31
|
backoff). Zero-dependency. Combined with session restore (below), a crash is
|
|
32
32
|
invisible — the room reconnects and the Claude session resumes where it left off.
|
|
33
33
|
```bash
|
|
34
|
-
npx thinkpool-pair <ROOM> --supervise -- claude
|
|
34
|
+
npx thinkpool-pair@latest <ROOM> --supervise -- claude
|
|
35
35
|
```
|
|
36
36
|
- **Boot-persistent service** — survive reboot/logout too. Installs the right
|
|
37
37
|
native service for your OS (launchd on macOS, systemd `--user` on Linux, a
|
|
38
38
|
Startup-folder script on Windows):
|
|
39
39
|
```bash
|
|
40
|
-
npx thinkpool-pair install-service <ROOM> -- claude # set and forget
|
|
41
|
-
npx thinkpool-pair uninstall-service <ROOM> # remove it
|
|
40
|
+
npx thinkpool-pair@latest install-service <ROOM> -- claude # set and forget
|
|
41
|
+
npx thinkpool-pair@latest uninstall-service <ROOM> # remove it
|
|
42
42
|
```
|
|
43
43
|
The service runs `npx thinkpool-pair@latest`, so it **auto-updates** — new
|
|
44
44
|
versions apply on the next restart, no re-install. (Linux: run
|
|
@@ -186,7 +186,7 @@ that format:
|
|
|
186
186
|
> not OpenRouter's Anthropic endpoint.
|
|
187
187
|
|
|
188
188
|
```bash
|
|
189
|
-
npx thinkpool-pair provider custom --base <url> --token <key> [--model <name>]
|
|
189
|
+
npx thinkpool-pair@latest provider custom --base <url> --token <key> [--model <name>]
|
|
190
190
|
```
|
|
191
191
|
|
|
192
192
|
Common base urls (the agent appends `/v1/messages`):
|
|
@@ -203,11 +203,11 @@ Common base urls (the agent appends `/v1/messages`):
|
|
|
203
203
|
> gateway is for. Avoid LiteLLM 1.82.7 / 1.82.8 (compromised releases); use a
|
|
204
204
|
> current version.
|
|
205
205
|
|
|
206
|
-
Show the current provider (`npx thinkpool-pair provider`), or reset back to your
|
|
206
|
+
Show the current provider (`npx thinkpool-pair@latest provider`), or reset back to your
|
|
207
207
|
regular Claude login:
|
|
208
208
|
|
|
209
209
|
```bash
|
|
210
|
-
npx thinkpool-pair provider anthropic
|
|
210
|
+
npx thinkpool-pair@latest provider anthropic
|
|
211
211
|
```
|
|
212
212
|
|
|
213
213
|
Provider config is stored in `~/.thinkpool-pair/provider.json` (mode 0600) and
|
package/account.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { installedAgentCommands } from './agent-detect.mjs'
|
|
|
24
24
|
import { hostMemoryAdmission } from './host-memory.mjs'
|
|
25
25
|
import { createPairBusBroker, mergePairRoomRoster, pairBusStatusFromRealtime, PAIR_BUS_STATUS } from './pair-bus.mjs'
|
|
26
26
|
import { clearSupervisorReady, supervisorPresenceEchoed, writeSupervisorReady } from './supervisor-ready.mjs'
|
|
27
|
+
import { PAIR_CLI, pairCli } from './command-guidance.mjs'
|
|
27
28
|
|
|
28
29
|
const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('./package.json', import.meta.url), 'utf8')).version } catch { return null } })()
|
|
29
30
|
|
|
@@ -121,7 +122,7 @@ export async function runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) {
|
|
|
121
122
|
process.stderr.write(`\n ◆ Link this device to your ThinkPool account.\n Open (signed in) and approve:\n\n ${url}\n\n code: ${user_code}\n\n Waiting…\n`)
|
|
122
123
|
const started = Date.now()
|
|
123
124
|
const poll = async () => {
|
|
124
|
-
if (Date.now() - started > 5 * 60 * 1000) { process.stderr.write(
|
|
125
|
+
if (Date.now() - started > 5 * 60 * 1000) { process.stderr.write(`\n ◇ link timed out — run \`${pairCli('login')}\` again.\n`); process.exit(1) }
|
|
125
126
|
try {
|
|
126
127
|
// A discarded `error` costs one tick — the loop retries in a second, and a
|
|
127
128
|
// permanent failure still lands on the "link timed out" branch above rather than
|
|
@@ -130,10 +131,10 @@ export async function runLogin(SUPABASE_URL, SUPABASE_ANON, WEB_BASE) {
|
|
|
130
131
|
const { data: r } = await sb.rpc('claim_device_code', { p_device_code: device_code })
|
|
131
132
|
if (r?.status === 'approved' && r.session?.refresh_token) {
|
|
132
133
|
saveAuth(r.session)
|
|
133
|
-
process.stderr.write(`\n ◆ linked as ${r.session.email || 'your account'}. You can close the browser tab.\n Now run:
|
|
134
|
+
process.stderr.write(`\n ◆ linked as ${r.session.email || 'your account'}. You can close the browser tab.\n Now run: ${PAIR_CLI}\n`)
|
|
134
135
|
process.exit(0)
|
|
135
136
|
}
|
|
136
|
-
if (r?.status === 'expired' || r?.status === 'not_found') { process.stderr.write(
|
|
137
|
+
if (r?.status === 'expired' || r?.status === 'not_found') { process.stderr.write(`\n ◇ link expired — run \`${pairCli('login')}\` again.\n`); process.exit(1) }
|
|
137
138
|
} catch { /* transient — keep polling */ }
|
|
138
139
|
setTimeout(poll, 3000)
|
|
139
140
|
}
|
|
@@ -397,18 +398,18 @@ export function scheduleTokenRefresh({ sb, session, onRefreshed, onReconnectRequ
|
|
|
397
398
|
|
|
398
399
|
export function runBind(room, dir) {
|
|
399
400
|
const r = (room || '').toUpperCase().trim()
|
|
400
|
-
if (!r) { console.error(
|
|
401
|
+
if (!r) { console.error(`usage: ${pairCli('bind', '<ROOM>', '<directory>')}`); process.exit(1) }
|
|
401
402
|
const abs = path.resolve(dir || '.')
|
|
402
403
|
if (!fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) { console.error(`✗ not a directory: ${abs}`); process.exit(1) }
|
|
403
404
|
bindDir(r, abs)
|
|
404
|
-
console.error(`✓ bound ${r} → ${abs}\n Run \`
|
|
405
|
+
console.error(`✓ bound ${r} → ${abs}\n Run \`${PAIR_CLI}\` (or restart it) to serve this session.`)
|
|
405
406
|
process.exit(0)
|
|
406
407
|
}
|
|
407
408
|
|
|
408
409
|
// ── account supervisor: a headless child bridge per bound room, in its dir ──
|
|
409
410
|
export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
410
411
|
if (!acquireSingleton()) {
|
|
411
|
-
console.error(
|
|
412
|
+
console.error(`\n ◇ An account bridge is already running on this machine\n (~/.thinkpool-pair/account.lock). Not starting a second — two would race\n your saved login. Stop the other one first, or share a single room with\n \`${pairCli('<ROOM>')}\`.\n`)
|
|
412
413
|
process.exit(0)
|
|
413
414
|
}
|
|
414
415
|
// We own account.lock now, so any previous readiness receipt is stale. A fresh
|
|
@@ -430,7 +431,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
430
431
|
// Reached ONLY when there is no saved login at all (not linked). Stand down CLEANLY
|
|
431
432
|
// with exit 0: launchd KeepAlive has SuccessfulExit=false, so exit 0 is NOT respawned
|
|
432
433
|
// — no crash storm for the "never logged in" case. The user links with `login`.
|
|
433
|
-
console.error(
|
|
434
|
+
console.error(`\n ◇ Not linked to a ThinkPool account on this machine.\n Link it: ${pairCli('login')}\n`)
|
|
434
435
|
process.exit(0)
|
|
435
436
|
}
|
|
436
437
|
if (auth.reconnectRequired) {
|
|
@@ -470,9 +471,9 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
470
471
|
? ' (auto-updating background service — survives reboot, tracks @latest)\n'
|
|
471
472
|
: isManagedService
|
|
472
473
|
? ' (background service — survives reboot, restarts on crash, pinned to this version)\n'
|
|
473
|
-
:
|
|
474
|
+
: ` (run hands-off + auto-updating: ${pairCli('install-service')})\n`
|
|
474
475
|
const _effDefault = loadDefaultDir() || claudeRecentProjectDir() || DEFAULT_DIR
|
|
475
|
-
process.stderr.write(`\n ◆ thinkpool-pair — account mode · ${email}\n New sessions auto-serve from ${_effDefault}${loadDefaultDir() ? ' (your default)' : (claudeRecentProjectDir() ? ' (your most-recent Claude Code project)' : '')}\n (change it:
|
|
476
|
+
process.stderr.write(`\n ◆ thinkpool-pair — account mode · ${email}\n New sessions auto-serve from ${_effDefault}${loadDefaultDir() ? ' (your default)' : (claudeRecentProjectDir() ? ' (your most-recent Claude Code project)' : '')}\n (change it: ${pairCli('set-default-dir', '<dir>')} · one room: ${pairCli('bind', '<code>', '<dir>')})\n${svcHint}`)
|
|
476
477
|
|
|
477
478
|
const children = new Map() // room -> child process
|
|
478
479
|
const childIdle = new Map() // room -> bool (last idle report: in the quiet window)
|
|
@@ -955,7 +956,7 @@ export async function runAccount(SUPABASE_URL, SUPABASE_ANON) {
|
|
|
955
956
|
const why = choice.reason === 'home'
|
|
956
957
|
? `no project directory set — refusing to serve it from your home folder (${choice.dir}); the agent would lose its session context and the room would show "${path.basename(choice.dir)}" as the repo.`
|
|
957
958
|
: 'no directory to serve it from.'
|
|
958
|
-
process.stderr.write(`\n ◇ ${room}: ${why}\n Bind it to its repo:
|
|
959
|
+
process.stderr.write(`\n ◇ ${room}: ${why}\n Bind it to its repo: ${pairCli('bind', room, '<repo-dir>')}\n`)
|
|
959
960
|
}
|
|
960
961
|
continue
|
|
961
962
|
}
|
package/bridge.mjs
CHANGED
|
@@ -68,6 +68,7 @@ import { requiresSdkAdmission, sdkSmokePassed } from './sdk-admission.mjs'
|
|
|
68
68
|
import { hermesUserInputResponse } from './question-response.mjs'
|
|
69
69
|
import { hostMemoryAdmission } from './host-memory.mjs'
|
|
70
70
|
import { queueAbortBarrier, waitForAbortBarrier } from './abort-turn-barrier.mjs'
|
|
71
|
+
import { PAIR_CLI, pairCli } from './command-guidance.mjs'
|
|
71
72
|
|
|
72
73
|
const STRUCTURED_MODES = new Set(['default', 'acceptEdits', 'plan', 'review', 'bypassPermissions'])
|
|
73
74
|
import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, FLOW_CODEX_CONDUCTOR_PROMPT, FLOW_CODEX_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
|
|
@@ -292,7 +293,7 @@ if (argv[0] === 'bind') { const { runBind } = await import('./account.mjs'); r
|
|
|
292
293
|
if (argv[0] === 'set-default-dir') {
|
|
293
294
|
const { saveDefaultDir } = await import('./auth-store.mjs')
|
|
294
295
|
let d = (argv[1] || '').trim(); if (d.startsWith('~')) d = d.replace(/^~/, os.homedir())
|
|
295
|
-
if (!d) { console.error(
|
|
296
|
+
if (!d) { console.error(` usage: ${pairCli('set-default-dir', '<dir>')}`); process.exit(1) }
|
|
296
297
|
saveDefaultDir(d)
|
|
297
298
|
console.log(` ◆ default project dir set → ${d}\n New/unbound sessions auto-serve from here — no per-session bind. Takes effect next serve tick.`)
|
|
298
299
|
process.exit(0)
|
|
@@ -437,7 +438,7 @@ function checkSdkCompat() {
|
|
|
437
438
|
if (!argv[0] || argv[0].startsWith('-')) { const { runAccount } = await import('./account.mjs'); await runAccount(SUPABASE_URL, SUPABASE_ANON) }
|
|
438
439
|
|
|
439
440
|
const room = (argv[0] || '').toUpperCase().trim()
|
|
440
|
-
if (!room) { console.error(
|
|
441
|
+
if (!room) { console.error(`usage: ${pairCli('<ROOM>', '[--headless]', '[--continue|--fresh]', '[-- <command…>]')} | ${PAIR_CLI} (account mode)`); process.exit(1) }
|
|
441
442
|
// Security (audit 2026-07-02): `room` becomes a filesystem path segment under
|
|
442
443
|
// ~/.thinkpool-pair (session-store) AND is interpolated into launchctl/systemd
|
|
443
444
|
// service labels. sessions.id is unconstrained TEXT, so refuse path-unsafe / shell-
|
|
@@ -549,7 +550,7 @@ if (dashIdx >= 0) {
|
|
|
549
550
|
}
|
|
550
551
|
} else if (!headless) {
|
|
551
552
|
if (installedAgents.length === 0) {
|
|
552
|
-
console.error(`\n No known coding-agent CLI found on your PATH.\n Install one (claude / codex / gemini / aider / cursor-agent / opencode …)\n or share a specific command:
|
|
553
|
+
console.error(`\n No known coding-agent CLI found on your PATH.\n Install one (claude / codex / gemini / aider / cursor-agent / opencode …)\n or share a specific command: ${pairCli(room, '--', '<your-command>')}\n`)
|
|
553
554
|
process.exit(1)
|
|
554
555
|
}
|
|
555
556
|
attachedCmd = await pickAgent(installedAgents)
|
|
@@ -831,7 +832,7 @@ let myServeUid = null // this bridge's authed uid (null = anon)
|
|
|
831
832
|
// 401 must NEVER serve, or the gate is bypassed (DEFECT 1). gateFailAction classifies.
|
|
832
833
|
if (gateFailAction(e) === 'refuse') {
|
|
833
834
|
process.stderr.write(refusalMessage({ room, name: null, reason: 'no-identity' }))
|
|
834
|
-
process.stderr.write(`\n ◇ (serve-consent: auth rejected at the gate — ${e.message}; the stored login is expired or revoked. Re-run:
|
|
835
|
+
process.stderr.write(`\n ◇ (serve-consent: auth rejected at the gate — ${e.message}; the stored login is expired or revoked. Re-run: ${pairCli('login')})\n`)
|
|
835
836
|
process.exit(0)
|
|
836
837
|
}
|
|
837
838
|
// Availability failure → fail OPEN (spec: never strand a user on a flaky fetch).
|
|
@@ -1402,7 +1403,7 @@ let standalonePairChannel = null // standalone owner bridge's direct paired-sess
|
|
|
1402
1403
|
// standalone bridge (no supervisor) or a disabled/timed-out request returns { error }.
|
|
1403
1404
|
function pairRequest(type, payload = {}, timeoutMs = 4000) {
|
|
1404
1405
|
if (process.env.TP_CROSSROOM_OFF === '1') return Promise.resolve({ error: 'Cross-session reach is turned off in this room.' })
|
|
1405
|
-
if (!IS_ACCOUNT_CHILD) return Promise.resolve({ error:
|
|
1406
|
+
if (!IS_ACCOUNT_CHILD) return Promise.resolve({ error: `Cross-session reach needs the ThinkPool account bridge running your sessions (${PAIR_CLI}). This room is running standalone, so it can only see its own terminals — use read_terminal.` })
|
|
1406
1407
|
const reqId = randomUUID()
|
|
1407
1408
|
return new Promise((resolve) => {
|
|
1408
1409
|
const timer = setTimeout(() => { pairWaiters.delete(reqId); resolve({ error: 'The other session did not respond in time.' }) }, timeoutMs)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Canonical, cache-busting invocation for every command printed to people.
|
|
2
|
+
export const PAIR_CLI = 'npx thinkpool-pair@latest'
|
|
3
|
+
|
|
4
|
+
export function pairCli(...args) {
|
|
5
|
+
return [PAIR_CLI, ...args.filter((arg) => arg !== undefined && arg !== null && String(arg).length > 0)]
|
|
6
|
+
.map(String)
|
|
7
|
+
.join(' ')
|
|
8
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "thinkpool-pair",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.320",
|
|
4
4
|
"description": "Share a local coding-agent CLI (Claude Code, Codex, Gemini, Aider, …) into a ThinkPool Code room, live.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"bridge.mjs",
|
|
11
|
+
"command-guidance.mjs",
|
|
11
12
|
"abort-turn-barrier.mjs",
|
|
12
13
|
"host-memory.mjs",
|
|
13
14
|
"sdk-smoke.mjs",
|
package/provider.mjs
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import os from 'node:os'
|
|
32
32
|
import fs from 'node:fs'
|
|
33
33
|
import path from 'node:path'
|
|
34
|
+
import { pairCli } from './command-guidance.mjs'
|
|
34
35
|
|
|
35
36
|
const DIR = path.join(os.homedir(), '.thinkpool-pair')
|
|
36
37
|
const FILE = path.join(DIR, 'provider.json')
|
|
@@ -68,7 +69,7 @@ export function applyProviderEnv() {
|
|
|
68
69
|
const authToken = process.env.TP_ANTHROPIC_AUTH_TOKEN || cfg.authToken
|
|
69
70
|
const model = process.env.TP_ANTHROPIC_MODEL || cfg.model
|
|
70
71
|
if (!baseUrl || !authToken) {
|
|
71
|
-
process.stderr.write(`\n ◇ provider "${provider}" is set but is missing baseUrl/authToken.\n Finish setup:
|
|
72
|
+
process.stderr.write(`\n ◇ provider "${provider}" is set but is missing baseUrl/authToken.\n Finish setup: ${pairCli('provider', 'custom', '--base', '<url>', '--token', '<key>')}\n Falling back to regular Claude for now.\n`)
|
|
72
73
|
return { provider: 'anthropic', source: 'incomplete' }
|
|
73
74
|
}
|
|
74
75
|
process.env.ANTHROPIC_BASE_URL = baseUrl
|
|
@@ -121,9 +122,9 @@ export async function runProvider(args) {
|
|
|
121
122
|
|
|
122
123
|
console.error(
|
|
123
124
|
'\n usage:\n' +
|
|
124
|
-
'
|
|
125
|
-
'
|
|
126
|
-
'
|
|
125
|
+
` ${pairCli('provider')} # show current\n` +
|
|
126
|
+
` ${pairCli('provider', 'custom', '--base', '<url>', '--token', '<key>', '[--model <m>]')} # any Anthropic-compatible endpoint\n` +
|
|
127
|
+
` ${pairCli('provider', 'anthropic')} # reset to Claude\n\n` +
|
|
127
128
|
' common base urls (SDK appends /v1/messages):\n' +
|
|
128
129
|
' Z.ai GLM → https://api.z.ai/api/anthropic\n' +
|
|
129
130
|
' OpenRouter → https://openrouter.ai/api\n'
|
package/serve-consent.mjs
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// so even an old bridge can no longer READ a grant (the column is gone) and will refuse
|
|
19
19
|
// as a non-owner. That closes the bypass.
|
|
20
20
|
|
|
21
|
+
import { pairCli } from './command-guidance.mjs'
|
|
22
|
+
|
|
21
23
|
// serveDecision — may THIS bridge serve THIS room?
|
|
22
24
|
// ownerId : code_sessions.owner_id (uuid | null)
|
|
23
25
|
// myUid : this bridge's authed user id (uuid | null) — null when the bridge is anon
|
|
@@ -109,7 +111,7 @@ export function refusalMessage ({ room, name, reason }) {
|
|
|
109
111
|
const label = name ? `"${name}"` : '(unnamed)'
|
|
110
112
|
if (reason === 'no-identity') {
|
|
111
113
|
return `\n ◇ Refusing to serve room ${room} — this bridge is not signed in.\n` +
|
|
112
|
-
` A room is served only by its owner's bridge. Sign in first:
|
|
114
|
+
` A room is served only by its owner's bridge. Sign in first: ${pairCli('login')}\n`
|
|
113
115
|
}
|
|
114
116
|
return `\n ◇ Refusing to serve room ${room} ${label} — it is owned by another person; only their bridge serves it.\n` +
|
|
115
117
|
` Pairing gives you a read-only view across the pair, not hosting rights. There is no override.\n`
|
package/service.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import path from 'node:path'
|
|
|
24
24
|
import { execSync } from 'node:child_process'
|
|
25
25
|
import { hostMemoryAdmission } from './host-memory.mjs'
|
|
26
26
|
import { readSupervisorReady, supervisorReadyMatches } from './supervisor-ready.mjs'
|
|
27
|
+
import { pairCli } from './command-guidance.mjs'
|
|
27
28
|
|
|
28
29
|
// Service identity. Account mode has no room → a single stable id so there's
|
|
29
30
|
// exactly one account service per machine (a second install replaces it).
|
|
@@ -499,10 +500,10 @@ export function installService(room, cmdArgs = [], { autoUpdate = false, version
|
|
|
499
500
|
? (process.platform === 'win32'
|
|
500
501
|
? 'auto-update ON: a new thinkpool-pair@latest applies on the next login/restart (Windows has no in-process self-update).'
|
|
501
502
|
: `auto-update ON: checks npm ~every 30 min and restarts to apply a new @latest once sessions are idle (≥${idleSecs}s quiet; they resume in place).`)
|
|
502
|
-
: `PINNED to ${version} (stable local runtime; restarts do not depend on npx or the network). To update: use "Restart & update the bridge" (or
|
|
503
|
+
: `PINNED to ${version} (stable local runtime; restarts do not depend on npx or the network). To update: use "Restart & update the bridge" (or run ${pairCli('install-service', room || undefined)}). Pass --auto-update to track @latest instead.`
|
|
503
504
|
const removeArg = room ? ` ${room}` : ''
|
|
504
505
|
const what = room ? `room ${room}` : 'your account (auto-serves every session)'
|
|
505
|
-
process.stderr.write(` ◆ ${what}\n ◆ ${process.platform === 'darwin' ? 'Launchd is completing and verifying the reload independently.' : a.note}\n ◆ ${updateNote}\n ◆ logs: ${path.join(a.logDir, `${slug(room)}.log`)}\n ◆ remove with:
|
|
506
|
+
process.stderr.write(` ◆ ${what}\n ◆ ${process.platform === 'darwin' ? 'Launchd is completing and verifying the reload independently.' : a.note}\n ◆ ${updateNote}\n ◆ logs: ${path.join(a.logDir, `${slug(room)}.log`)}\n ◆ remove with: ${pairCli('uninstall-service')}${removeArg}\n\n`)
|
|
506
507
|
return true
|
|
507
508
|
}
|
|
508
509
|
|